summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2017-12-15 17:14:26 +0800
committerLin Jen-Shin <godfat@godfat.org>2017-12-15 17:14:26 +0800
commit59ac184fcf64f1812fbfd88a00ea029ca3c1f4e7 (patch)
tree5db0594a6f568f02b4f54c6bf4eabe01229a9f95 /spec
parent85be6d83be4632c76760e373da131a90afb093b9 (diff)
parent1baea77438779e74657b49ca26810d6c8f041b41 (diff)
downloadgitlab-ce-59ac184fcf64f1812fbfd88a00ea029ca3c1f4e7.tar.gz
Merge remote-tracking branch 'upstream/master' into no-ivar-in-modules
* upstream/master: (671 commits) Make rubocop happy Use guard clause Improve language Prettify Use temp branch Pass info about who started the job and which job triggered it Docs: add indexes for monitoring and performance monitoring clearer-documentation-on-inline-diffs Add docs for commit diff discussion in merge requests sorting for tags api Clear BatchLoader after each spec to prevent holding onto records longer than necessary Include project in BatchLoader key to prevent returning blobs for the wrong project moved lfs_blob_ids method into ExtractsPath module Converted JS modules into exported modules spec fixes Bump gitlab-shell version to 5.10.3 Clear caches before updating MR diffs Use new Ruby version 2.4 in GitLab QA images moved lfs blob fetch from extractspath file Update GitLab QA dependencies ...
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/storage_check_spec.rb13
-rw-r--r--spec/controllers/admin/health_check_controller_spec.rb4
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb19
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb10
-rw-r--r--spec/controllers/health_controller_spec.rb42
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb12
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb40
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb185
-rw-r--r--spec/controllers/projects/clusters/user_controller_spec.rb89
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb395
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb18
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb23
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb3
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb20
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb15
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb20
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb247
-rw-r--r--spec/controllers/projects_controller_spec.rb58
-rw-r--r--spec/factories/appearances.rb1
-rw-r--r--spec/factories/ci/builds.rb39
-rw-r--r--spec/factories/ci/job_artifacts.rb30
-rw-r--r--spec/factories/clusters/clusters.rb (renamed from spec/factories/clusters/cluster.rb)17
-rw-r--r--spec/factories/commits.rb19
-rw-r--r--spec/factories/notes.rb10
-rw-r--r--spec/factories/uploads.rb16
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/admin/admin_appearance_spec.rb33
-rw-r--r--spec/features/admin/admin_health_check_spec.rb12
-rw-r--r--spec/features/auto_deploy_spec.rb88
-rw-r--r--spec/features/commits_spec.rb8
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb2
-rw-r--r--spec/features/groups/labels/user_sees_links_to_issuables.rb15
-rw-r--r--spec/features/groups/members/manage_members.rb21
-rw-r--r--spec/features/issuables/discussion_lock_spec.rb2
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb108
-rw-r--r--spec/features/issues_spec.rb14
-rw-r--r--spec/features/logout_spec.rb22
-rw-r--r--spec/features/markdown_spec.rb3
-rw-r--r--spec/features/merge_requests/image_diff_notes.rb12
-rw-r--r--spec/features/merge_requests/mini_pipeline_graph_spec.rb4
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb15
-rw-r--r--spec/features/merge_requests/versions_spec.rb105
-rw-r--r--spec/features/merge_requests/widget_spec.rb12
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/projects/clusters/applications_spec.rb107
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb138
-rw-r--r--spec/features/projects/clusters/interchangeability_spec.rb16
-rw-r--r--spec/features/projects/clusters/user_spec.rb102
-rw-r--r--spec/features/projects/clusters_spec.rb212
-rw-r--r--spec/features/projects/environments/environment_spec.rb51
-rw-r--r--spec/features/projects/environments/environments_spec.rb33
-rw-r--r--spec/features/projects/features_visibility_spec.rb6
-rw-r--r--spec/features/projects/issuable_templates_spec.rb4
-rw-r--r--spec/features/projects/jobs_spec.rb8
-rw-r--r--spec/features/projects/labels/user_sees_links_to_issuables.rb75
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb30
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb101
-rw-r--r--spec/features/projects/snippets_spec.rb5
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb28
-rw-r--r--spec/finders/clusters_finder_spec.rb31
-rw-r--r--spec/fixtures/api/schemas/contributor.json18
-rw-r--r--spec/fixtures/api/schemas/contributors.json4
-rw-r--r--spec/fixtures/emails/valid_new_merge_request.eml20
-rw-r--r--spec/fixtures/emails/valid_new_merge_request_no_description.eml18
-rw-r--r--spec/fixtures/emails/valid_new_merge_request_no_subject.eml18
-rw-r--r--spec/fixtures/markdown.md.erb17
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb100
-rw-r--r--spec/helpers/boards_helper_spec.rb21
-rw-r--r--spec/helpers/button_helper_spec.rb83
-rw-r--r--spec/helpers/labels_helper_spec.rb63
-rw-r--r--spec/helpers/markup_helper_spec.rb2
-rw-r--r--spec/helpers/members_helper_spec.rb8
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb17
-rw-r--r--spec/helpers/preferences_helper_spec.rb74
-rw-r--r--spec/helpers/tree_helper_spec.rb1
-rw-r--r--spec/javascripts/activities_spec.js4
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js76
-rw-r--r--spec/javascripts/boards/boards_store_spec.js1
-rw-r--r--spec/javascripts/boards/issue_spec.js7
-rw-r--r--spec/javascripts/boards/list_spec.js1
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js18
-rw-r--r--spec/javascripts/clusters/clusters_index_spec.js58
-rw-r--r--spec/javascripts/datetime_utility_spec.js186
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js2
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js14
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js3
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js10
-rw-r--r--spec/javascripts/fixtures/clusters.rb15
-rw-r--r--spec/javascripts/fly_out_nav_spec.js4
-rw-r--r--spec/javascripts/gl_dropdown_spec.js6
-rw-r--r--spec/javascripts/groups/components/app_spec.js6
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js6
-rw-r--r--spec/javascripts/groups/components/item_actions_spec.js24
-rw-r--r--spec/javascripts/image_diff/helpers/badge_helper_spec.js9
-rw-r--r--spec/javascripts/image_diff/helpers/utils_helper_spec.js10
-rw-r--r--spec/javascripts/issuable_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js64
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js30
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js1
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/job_spec.js20
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js2
-rw-r--r--spec/javascripts/merge_request_spec.js4
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js13
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js29
-rw-r--r--spec/javascripts/monitoring/graph_spec.js14
-rw-r--r--spec/javascripts/monitoring/mock_data.js6
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js (renamed from spec/javascripts/notes/components/issue_comment_form_spec.js)10
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js (renamed from spec/javascripts/notes/components/issue_note_actions_spec.js)4
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js (renamed from spec/javascripts/notes/components/issue_note_app_spec.js)87
-rw-r--r--spec/javascripts/notes/components/note_attachment_spec.js (renamed from spec/javascripts/notes/components/issue_note_attachment_spec.js)4
-rw-r--r--spec/javascripts/notes/components/note_awards_list_spec.js (renamed from spec/javascripts/notes/components/issue_note_awards_list_spec.js)8
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js (renamed from spec/javascripts/notes/components/issue_note_body_spec.js)6
-rw-r--r--spec/javascripts/notes/components/note_edited_text_spec.js (renamed from spec/javascripts/notes/components/issue_note_edited_text_spec.js)6
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js (renamed from spec/javascripts/notes/components/issue_note_form_spec.js)6
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js (renamed from spec/javascripts/notes/components/issue_note_header_spec.js)6
-rw-r--r--spec/javascripts/notes/components/note_signed_out_widget_spec.js (renamed from spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js)6
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js (renamed from spec/javascripts/notes/components/issue_discussion_spec.js)8
-rw-r--r--spec/javascripts/notes/components/noteable_note_spec.js (renamed from spec/javascripts/notes/components/issue_note_spec.js)21
-rw-r--r--spec/javascripts/notes/mock_data.js336
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js8
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js10
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js10
-rw-r--r--spec/javascripts/notes_spec.js12
-rw-r--r--spec/javascripts/pager_spec.js7
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js96
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js5
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js10
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js81
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js84
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js120
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js176
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js7
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js128
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js5
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js9
-rw-r--r--spec/javascripts/right_sidebar_spec.js2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js9
-rw-r--r--spec/javascripts/sidebar/mock_data.js82
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js45
-rw-r--r--spec/javascripts/sidebar/sidebar_mediator_spec.js32
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js6
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js9
-rw-r--r--spec/javascripts/syntax_highlight_spec.js74
-rw-r--r--spec/javascripts/test_bundle.js12
-rw-r--r--spec/javascripts/todos_spec.js5
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/modal_spec.js12
-rw-r--r--spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js6
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js91
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb21
-rw-r--r--spec/lib/backup/manager_spec.rb (renamed from spec/lib/gitlab/backup/manager_spec.rb)0
-rw-r--r--spec/lib/backup/repository_spec.rb69
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb8
-rw-r--r--spec/lib/banzai/filter/abstract_reference_filter_spec.rb38
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb47
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb30
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb27
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb19
-rw-r--r--spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb519
-rw-r--r--spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb242
-rw-r--r--spec/lib/gitlab/backup/repository_spec.rb117
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb17
-rw-r--r--spec/lib/gitlab/bare_repository_import/repository_spec.rb7
-rw-r--r--spec/lib/gitlab/checks/project_moved_spec.rb81
-rw-r--r--spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb185
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb28
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb8
-rw-r--r--spec/lib/gitlab/database_spec.rb38
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb96
-rw-r--r--spec/lib/gitlab/email/handler_spec.rb17
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb29
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb33
-rw-r--r--spec/lib/gitlab/git/storage/checker_spec.rb132
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb144
-rw-r--r--spec/lib/gitlab/git/storage/failure_info_spec.rb70
-rw-r--r--spec/lib/gitlab/git/storage/health_spec.rb3
-rw-r--r--spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb4
-rw-r--r--spec/lib/gitlab/git_access_spec.rb60
-rw-r--r--spec/lib/gitlab/git_spec.rb25
-rw-r--r--spec/lib/gitlab/identifier_spec.rb4
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb32
-rw-r--r--spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb23
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb23
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb9
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb39
-rw-r--r--spec/lib/gitlab/shell_spec.rb8
-rw-r--r--spec/lib/gitlab/sidekiq_config_spec.rb45
-rw-r--r--spec/lib/gitlab/sidekiq_versioning/manager_spec.rb22
-rw-r--r--spec/lib/gitlab/sidekiq_versioning_spec.rb44
-rw-r--r--spec/lib/gitlab/storage_check/cli_spec.rb19
-rw-r--r--spec/lib/gitlab/storage_check/gitlab_caller_spec.rb46
-rw-r--r--spec/lib/gitlab/storage_check/option_parser_spec.rb31
-rw-r--r--spec/lib/gitlab/storage_check/response_spec.rb54
-rw-r--r--spec/lib/gitlab/tcp_checker_spec.rb32
-rw-r--r--spec/lib/gitlab/utils/strong_memoize_spec.rb12
-rw-r--r--spec/lib/gitlab/utils_spec.rb10
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb8
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb24
-rw-r--r--spec/mailers/notify_spec.rb44
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb11
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb39
-rw-r--r--spec/migrations/remove_assignee_id_from_issue_spec.rb37
-rw-r--r--spec/migrations/track_untracked_uploads_spec.rb27
-rw-r--r--spec/models/appearance_spec.rb3
-rw-r--r--spec/models/application_setting_spec.rb15
-rw-r--r--spec/models/ci/build_spec.rb398
-rw-r--r--spec/models/ci/job_artifact_spec.rb74
-rw-r--r--spec/models/ci/pipeline_spec.rb102
-rw-r--r--spec/models/clusters/cluster_spec.rb23
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb192
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb20
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb29
-rw-r--r--spec/models/environment_spec.rb51
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb104
-rw-r--r--spec/models/namespace_spec.rb40
-rw-r--r--spec/models/note_spec.rb24
-rw-r--r--spec/models/personal_access_token_spec.rb25
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb95
-rw-r--r--spec/models/project_statistics_spec.rb26
-rw-r--r--spec/models/repository_spec.rb274
-rw-r--r--spec/models/route_spec.rb104
-rw-r--r--spec/models/user_spec.rb218
-rw-r--r--spec/policies/group_policy_spec.rb27
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb40
-rw-r--r--spec/presenters/group_member_presenter_spec.rb138
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb2
-rw-r--r--spec/presenters/project_member_presenter_spec.rb138
-rw-r--r--spec/requests/api/circuit_breakers_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb76
-rw-r--r--spec/requests/api/internal_spec.rb38
-rw-r--r--spec/requests/api/issues_spec.rb14
-rw-r--r--spec/requests/api/protected_branches_spec.rb36
-rw-r--r--spec/requests/api/repositories_spec.rb22
-rw-r--r--spec/requests/api/runner_spec.rb15
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/tags_spec.rb38
-rw-r--r--spec/requests/git_http_spec.rb8
-rw-r--r--spec/rubocop/cop/include_sidekiq_worker_spec.rb31
-rw-r--r--spec/rubocop/cop/migration/remove_column_spec.rb68
-rw-r--r--spec/rubocop/cop/sidekiq_options_queue_spec.rb26
-rw-r--r--spec/serializers/merge_request_entity_spec.rb30
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb4
-rw-r--r--spec/services/base_count_service_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb73
-rw-r--r--spec/services/ci/register_job_service_spec.rb118
-rw-r--r--spec/services/ci/retry_build_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb2
-rw-r--r--spec/services/clusters/create_service_spec.rb75
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb1
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb12
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb8
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb14
-rw-r--r--spec/services/notification_service_spec.rb28
-rw-r--r--spec/services/projects/count_service_spec.rb12
-rw-r--r--spec/services/projects/fork_service_spec.rb353
-rw-r--r--spec/services/projects/update_pages_service_spec.rb116
-rw-r--r--spec/services/projects/update_service_spec.rb45
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/services/users/keys_count_service_spec.rb10
-rw-r--r--spec/services/web_hook_service_spec.rb2
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/batch_loader.rb5
-rw-r--r--spec/support/capybara.rb36
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb2
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb28
-rw-r--r--spec/support/query_recorder.rb19
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb240
-rw-r--r--spec/support/shared_examples/throttled_touch.rb20
-rw-r--r--spec/support/stored_repositories.rb21
-rw-r--r--spec/support/stub_configuration.rb2
-rw-r--r--spec/support/stub_gitlab_calls.rb6
-rw-r--r--spec/support/test_env.rb5
-rw-r--r--spec/support/track_untracked_uploads_helpers.rb20
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/uploaders/artifact_uploader_spec.rb61
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb51
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb77
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb21
-rw-r--r--spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb15
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb22
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb36
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/_blob_item.html.haml_spec.rb40
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb1
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb1
-rw-r--r--spec/workers/background_migration_worker_spec.rb31
-rw-r--r--spec/workers/concerns/application_worker_spec.rb66
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb8
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb8
-rw-r--r--spec/workers/concerns/dedicated_sidekiq_queue_spec.rb20
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb4
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb8
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb17
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb8
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb50
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb22
-rw-r--r--spec/workers/post_receive_spec.rb12
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb25
-rw-r--r--spec/workers/repository_fork_worker_spec.rb77
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb6
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb38
326 files changed, 10162 insertions, 3098 deletions
diff --git a/spec/bin/storage_check_spec.rb b/spec/bin/storage_check_spec.rb
new file mode 100644
index 00000000000..02f6fcb6e3a
--- /dev/null
+++ b/spec/bin/storage_check_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe 'bin/storage_check' do
+ it 'is executable' do
+ command = %w[bin/storage_check -t unix://the/path/to/a/unix-socket.sock -i 10 -d]
+ expected_output = 'Checking unix://the/path/to/a/unix-socket.sock every 10 seconds'
+
+ output, status = Gitlab::Popen.popen(command, Rails.root.to_s)
+
+ expect(status).to eq(0)
+ expect(output).to include(expected_output)
+ end
+end
diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb
index 0b8e0c8a065..d15ee0021d9 100644
--- a/spec/controllers/admin/health_check_controller_spec.rb
+++ b/spec/controllers/admin/health_check_controller_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Admin::HealthCheckController, broken_storage: true do
+describe Admin::HealthCheckController do
let(:admin) { create(:admin) }
before do
@@ -17,7 +17,7 @@ describe Admin::HealthCheckController, broken_storage: true do
describe 'POST reset_storage_health' do
it 'resets all storage health information' do
- expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+ expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!)
post :reset_storage_health
end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 9c6d584f59b..362d5cc4514 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -62,6 +62,25 @@ describe Groups::GroupMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:group_member, :access_request, group: group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, group_member: { access_level: value },
+ group_id: group,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:group_member, :developer, group: group) }
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
new file mode 100644
index 00000000000..67a11e56e94
--- /dev/null
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe Groups::UploadsController do
+ let(:model) { create(:group, :public) }
+ let(:params) do
+ { group_id: model }
+ end
+
+ it_behaves_like 'handle uploads'
+end
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 9e9cf4f2c1f..95946def5f9 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -14,6 +14,48 @@ describe HealthController do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
+ describe '#storage_check' do
+ before do
+ allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
+ end
+
+ subject { post :storage_check }
+
+ it 'checks all the configured storages' do
+ expect(Gitlab::Git::Storage::Checker).to receive(:check_all).and_call_original
+
+ subject
+ end
+
+ it 'returns the check interval' do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ stub_application_setting(circuitbreaker_check_interval: 10)
+
+ subject
+
+ expect(json_response['check_interval']).to eq(10)
+ end
+
+ context 'with failing storages', :broken_storage do
+ before do
+ stub_storage_settings(
+ broken: { path: 'tmp/tests/non-existent-repositories' }
+ )
+ end
+
+ it 'includes the failure information' do
+ subject
+
+ expected_results = [
+ { 'storage' => 'broken', 'success' => false },
+ { 'storage' => 'default', 'success' => true }
+ ]
+
+ expect(json_response['results']).to eq(expected_results)
+ end
+ end
+ end
+
describe '#readiness' do
shared_context 'endpoint responding with readiness data' do
let(:request_params) { {} }
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 84cde33d944..d6ccb92c54b 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -14,6 +14,12 @@ describe Projects::BoardsController do
expect { list_boards }.to change(project.boards, :count).by(1)
end
+ it 'sets boards_endpoint instance variable to a boards path' do
+ list_boards
+
+ expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
+ end
+
context 'when format is HTML' do
it 'renders template' do
list_boards
@@ -59,6 +65,12 @@ describe Projects::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, project: project) }
+ it 'sets boards_endpoint instance variable to a boards path' do
+ read_board board: board
+
+ expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
+ end
+
context 'when format is HTML' do
it 'renders template' do
read_board board: board
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 973d6fed288..d731200f70f 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -113,22 +113,38 @@ describe Projects::BranchesController do
expect(response).to redirect_to project_tree_path(project, branch)
end
- it 'redirects to autodeploy setup page' do
- result = { status: :success, branch: double(name: branch) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'redirects to autodeploy setup page' do
+ result = { status: :success, branch: double(name: branch) }
+
+ expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
+ expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+
+ post :create,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ branch_name: branch,
+ issue_iid: issue.iid
+
+ expect(response.location).to include(project_new_blob_path(project, branch))
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
- project.services << build(:kubernetes_service)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ before do
+ project.services << build(:kubernetes_service)
+ end
- expect_any_instance_of(CreateBranchService).to receive(:execute).and_return(result)
- expect(SystemNoteService).to receive(:new_issue_branch).and_return(true)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- branch_name: branch,
- issue_iid: issue.iid
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ end
- expect(response.location).to include(project_new_blob_path(project, branch))
- expect(response).to have_gitlab_http_status(302)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
new file mode 100644
index 00000000000..ee7928beb7e
--- /dev/null
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Projects::Clusters::GcpController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ set(:project) { create(:project) }
+
+ describe 'GET login' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(gcp_new_project_clusters_path(project))
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ go
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :login, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'GET new' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(go).to redirect_to(gcp_login_project_clusters_path(project)) }
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'POST create' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: '111'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ context 'when creates a cluster on gke' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_gcp
+ expect(project.clusters.first).to be_kubernetes
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it 'redirects to login page' do
+ expect(go).to redirect_to(gcp_login_project_clusters_path(project))
+ end
+ end
+
+ context 'when access token is not stored in session' do
+ it 'redirects to login page' do
+ expect(go).to redirect_to(gcp_login_project_clusters_path(project))
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb
new file mode 100644
index 00000000000..913976d187f
--- /dev/null
+++ b/spec/controllers/projects/clusters/user_controller_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Projects::Clusters::UserController do
+ include AccessMatchersForController
+
+ set(:project) { create(:project) }
+
+ describe 'GET new' do
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'has new object' do
+ go
+
+ expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ get :new, namespace_id: project.namespace, project_id: project
+ end
+ end
+
+ describe 'POST create' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ namespace: 'aaa'
+ }
+ }
+ }
+ end
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_user
+ expect(project.clusters.first).to be_kubernetes
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { go }.to be_allowed_for(:admin) }
+ it { expect { go }.to be_allowed_for(:owner).of(project) }
+ it { expect { go }.to be_allowed_for(:master).of(project) }
+ it { expect { go }.to be_denied_for(:developer).of(project) }
+ it { expect { go }.to be_denied_for(:reporter).of(project) }
+ it { expect { go }.to be_denied_for(:guest).of(project) }
+ it { expect { go }.to be_denied_for(:user) }
+ it { expect { go }.to be_denied_for(:external) }
+ end
+
+ def go
+ post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ end
+ end
+end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index ca2bcb2b5ae..a3b13647c92 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -4,6 +4,8 @@ describe Projects::ClustersController do
include AccessMatchersForController
include GoogleApi::CloudPlatformHelpers
+ set(:project) { create(:project) }
+
describe 'GET index' do
describe 'functionality' do
let(:user) { create(:user) }
@@ -13,213 +15,50 @@ describe Projects::ClustersController do
sign_in(user)
end
- context 'when project has a cluster' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
- end
-
- context 'when project does not have a cluster' do
+ context 'when project has one or more clusters' do
let(:project) { create(:project) }
-
- it { expect(go).to redirect_to(new_project_cluster_path(project)) }
- end
- end
-
- describe 'security' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :index, namespace_id: project.namespace.to_param, project_id: project
- end
- end
-
- describe 'GET login' do
- let(:project) { create(:project) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when omniauth has been configured' do
- let(:key) { 'secere-key' }
-
- let(:session_key_for_redirect_uri) do
- GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
- end
-
- before do
- allow(SecureRandom).to receive(:hex).and_return(key)
- end
-
- it 'has authorize_url' do
+ let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ it 'lists available clusters' do
go
- expect(assigns(:authorize_url)).to include(key)
- expect(session[session_key_for_redirect_uri]).to eq(providers_gcp_new_project_clusters_url(project))
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
- end
- context 'when omniauth has not configured' do
- before do
- stub_omniauth_setting(providers: [])
- end
+ context 'when page is specified' do
+ let(:last_page) { project.clusters.page.total_pages }
- it 'does not have authorize_url' do
- go
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, projects: [project])
+ get :index, namespace_id: project.namespace, project_id: project, page: last_page
+ end
- expect(assigns(:authorize_url)).to be_nil
+ it 'redirects to the page' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
end
end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
- def go
- get :login, namespace_id: project.namespace, project_id: project
- end
- end
-
- shared_examples 'requires to login' do
- it 'redirects to create a cluster' do
- subject
-
- expect(response).to redirect_to(login_project_clusters_path(project))
- end
- end
-
- describe 'GET new_gcp' do
- let(:project) { create(:project) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- end
+ context 'when project does not have a cluster' do
+ let(:project) { create(:project) }
- it 'has new object' do
+ it 'returns an empty state page' do
go
- expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
- end
- end
-
- context 'when access token is expired' do
- before do
- stub_google_api_expired_token
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index, partial: :empty_state)
+ expect(assigns(:clusters)).to eq([])
end
-
- it { expect(go).to redirect_to(login_project_clusters_path(project)) }
- end
-
- context 'when access token is not stored in session' do
- it { expect(go).to redirect_to(login_project_clusters_path(project)) }
end
end
describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :new_gcp, namespace_id: project.namespace, project_id: project
- end
- end
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- describe 'POST create' do
- let(:project) { create(:project) }
-
- let(:params) do
- {
- cluster: {
- name: 'new-cluster',
- provider_type: :gcp,
- provider_gcp_attributes: {
- gcp_project_id: '111'
- }
- }
- }
- end
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when access token is valid' do
- before do
- stub_google_api_validate_token
- end
-
- context 'when creates a cluster on gke' do
- it 'creates a new cluster' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { go }.to change { Clusters::Cluster.count }
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
- end
- end
- end
-
- context 'when access token is expired' do
- before do
- stub_google_api_expired_token
- end
-
- it 'redirects to login page' do
- expect(go).to redirect_to(login_project_clusters_path(project))
- end
- end
-
- context 'when access token is not stored in session' do
- it 'redirects to login page' do
- expect(go).to redirect_to(login_project_clusters_path(project))
- end
- end
- end
-
- describe 'security' do
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
@@ -231,13 +70,12 @@ describe Projects::ClustersController do
end
def go
- post :create, params.merge(namespace_id: project.namespace, project_id: project)
+ get :index, namespace_id: project.namespace.to_param, project_id: project
end
end
describe 'GET status' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
- let(:project) { cluster.project }
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
describe 'functionality' do
let(:user) { create(:user) }
@@ -275,8 +113,7 @@ describe Projects::ClustersController do
end
describe 'GET show' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
describe 'functionality' do
let(:user) { create(:user) }
@@ -313,10 +150,8 @@ describe Projects::ClustersController do
end
describe 'PUT update' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let(:user) { create(:user) }
before do
@@ -324,10 +159,16 @@ describe Projects::ClustersController do
sign_in(user)
end
- context 'when update enabled' do
+ context 'when changing parameters' do
let(:params) do
{
- cluster: { enabled: false }
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
}
end
@@ -335,13 +176,19 @@ describe Projects::ClustersController do
go
cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
end
+ it "does not change cluster name" do
+ go
+
+ expect(cluster.name).to eq('test-cluster')
+ end
+
context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
it "rejects changes" do
go
@@ -354,11 +201,95 @@ describe Projects::ClustersController do
end
end
+ context 'when cluster is provided by user' do
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
+ }
+ end
+
+ it "updates and redirects back to show page" do
+ go_json
+
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ platform_kubernetes_attributes: {
+ namespace: 'my invalid namespace #@'
+ }
+ }
+ }
+ end
+
+ it "rejects changes" do
+ go_json
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ context 'when format is html' do
+ context 'when update enabled' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
+ }
+ end
+
+ it "updates and redirects back to show page" do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+ end
+ end
+
describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
let(:params) do
- {
- cluster: { enabled: false }
- }
+ { cluster: { enabled: false } }
end
it { expect { go }.to be_allowed_for(:admin) }
@@ -376,12 +307,16 @@ describe Projects::ClustersController do
project_id: project,
id: cluster)
end
- end
- describe 'delete update' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
+ def go_json
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster,
+ format: :json)
+ end
+ end
+ describe 'DELETE destroy' do
describe 'functionality' do
let(:user) { create(:user) }
@@ -390,31 +325,37 @@ describe Projects::ClustersController do
sign_in(user)
end
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- expect(response).to redirect_to(project_clusters_path(project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
- end
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :project, :providing_by_gcp) }
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
+ end
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
- expect(response).to redirect_to(project_clusters_path(project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ it "destroys and redirects back to clusters list" do
+ expect { go }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(project_clusters_path(project))
+ expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
+ end
end
end
- context 'when provider is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
it "destroys and redirects back to clusters list" do
expect { go }
@@ -429,6 +370,8 @@ describe Projects::ClustersController do
end
describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
it { expect { go }.to be_allowed_for(:admin) }
it { expect { go }.to be_allowed_for(:owner).of(project) }
it { expect { go }.to be_allowed_for(:master).of(project) }
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index fd90c0d8bad..694c64ae1ad 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -132,6 +132,22 @@ describe Projects::CommitController do
expect(response).to be_success
end
end
+
+ context 'in the context of a merge_request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:commit) { merge_request.commits.first }
+
+ it 'prepare diff notes in the context of the merge request' do
+ go(id: commit.id, merge_request_iid: merge_request.iid)
+
+ expect(assigns(:new_diff_note_attrs)).to eq({
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ commit_id: commit.id
+ })
+ expect(response).to be_ok
+ end
+ end
end
describe 'GET branches' do
@@ -323,7 +339,7 @@ describe Projects::CommitController do
context 'when the commit does not exist' do
before do
- diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit.id.reverse, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 4dbbaecdd6d..c5d08cb0b9d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -272,6 +272,20 @@ describe Projects::IssuesController do
expect(response).to have_http_status(:ok)
expect(issue.reload.title).to eq('New title')
end
+
+ context 'when Akismet is enabled and the issue is identified as spam' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ end
+
+ it 'renders json with recaptcha_html' do
+ subject
+
+ expect(JSON.parse(response.body)).to have_key('recaptcha_html')
+ end
+ end
end
context 'when user does not have access to update issue' do
@@ -504,17 +518,16 @@ describe Projects::IssuesController do
expect(spam_logs.first.recaptcha_verified).to be_falsey
end
- it 'renders json errors' do
+ it 'renders recaptcha_html json response' do
update_issue
- expect(json_response)
- .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
+ expect(json_response).to have_key('recaptcha_html')
end
- it 'returns 422 status' do
+ it 'returns 200 status' do
update_issue
- expect(response).to have_gitlab_http_status(422)
+ expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 18a70bec103..ba97ccfbbd4 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -100,7 +100,8 @@ describe Projects::MergeRequests::DiffsController do
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest',
- noteable_id: merge_request.id)
+ noteable_id: merge_request.id,
+ commit_id: nil)
end
it 'only renders the diffs for the path given' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index bfdad85c082..51d5d6a52b3 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -324,12 +324,12 @@ describe Projects::MergeRequestsController do
end
context 'when the pipeline succeeds is passed' do
- def merge_when_pipeline_succeeds
- post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+ let!(:head_pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
end
- before do
- create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
+ def merge_when_pipeline_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end
it 'returns :merge_when_pipeline_succeeds' do
@@ -354,6 +354,18 @@ describe Projects::MergeRequestsController do
project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end
+ context 'and head pipeline is not the current one' do
+ before do
+ head_pipeline.update(sha: 'not_current_sha')
+ end
+
+ it 'returns :failed' do
+ merge_when_pipeline_succeeds
+
+ expect(json_response).to eq('status' => 'failed')
+ end
+ end
+
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index b2d83a02290..1cc488bef32 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -16,14 +16,13 @@ describe Projects::PipelinesSettingsController do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
- project: { auto_devops_attributes: params,
- run_auto_devops_pipeline_implicit: 'false',
- run_auto_devops_pipeline_explicit: auto_devops_pipeline }
+ project: {
+ auto_devops_attributes: params
+ }
end
context 'when updating the auto_devops settings' do
let(:params) { { enabled: '', domain: 'mepmep.md' } }
- let(:auto_devops_pipeline) { 'false' }
it 'redirects to the settings page' do
subject
@@ -44,7 +43,9 @@ describe Projects::PipelinesSettingsController do
end
context 'when run_auto_devops_pipeline is true' do
- let(:auto_devops_pipeline) { 'true' }
+ before do
+ expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true)
+ end
it 'queues a CreatePipelineWorker' do
expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
@@ -54,7 +55,9 @@ describe Projects::PipelinesSettingsController do
end
context 'when run_auto_devops_pipeline is not true' do
- let(:auto_devops_pipeline) { 'false' }
+ before do
+ expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false)
+ end
it 'does not queue a CreatePipelineWorker' do
expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args)
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index a34dc27a5ed..290dba0610a 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -66,6 +66,26 @@ describe Projects::ProjectMembersController do
end
end
+ describe 'PUT update' do
+ let(:requester) { create(:project_member, :access_request, project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ Gitlab::Access.options.each do |label, value|
+ it "can change the access level to #{label}" do
+ xhr :put, :update, project_member: { access_level: value },
+ namespace_id: project.namespace,
+ project_id: project,
+ id: requester
+
+ expect(requester.reload.human_access).to eq(label)
+ end
+ end
+ end
+
describe 'DELETE destroy' do
let(:member) { create(:project_member, :developer, project: project) }
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index c2550b1efa7..d572085661d 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -1,247 +1,10 @@
-require('spec_helper')
+require 'spec_helper'
describe Projects::UploadsController do
- let(:project) { create(:project) }
- 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') }
-
- describe "POST #create" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- context "without params['file']" do
- it "returns an error" do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- format: :json
- expect(response).to have_gitlab_http_status(422)
- end
- end
-
- context 'with valid image' do
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- file: jpg,
- format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/uploads"
- end
-
- # NOTE: This is as close as we're getting to an Integration test for this
- # behavior. We're avoiding a proper Feature test because those should be
- # testing things entirely user-facing, which the Upload model is very much
- # not.
- it 'creates a corresponding Upload record' do
- upload = Upload.last
-
- aggregate_failures do
- expect(upload).to exist
- expect(upload.model).to eq project
- end
- end
- end
-
- context 'with valid non-image file' do
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- file: txt,
- format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/uploads"
- end
- end
+ let(:model) { create(:project, :public) }
+ let(:params) do
+ { namespace_id: model.namespace.to_param, project_id: model }
end
- describe "GET #show" do
- let(:go) do
- get :show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- secret: "123456",
- filename: "image.jpg"
- end
-
- context "when the project is public" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- 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
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- 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
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
-
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- 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)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "when the file doesn't exist" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
- 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
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when the user doesn't have access to the project" 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)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
- end
- end
+ it_behaves_like 'handle uploads'
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index e7ab714c550..e61187fb518 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -261,6 +261,27 @@ describe ProjectsController do
expect(response).to redirect_to(namespace_project_path)
end
end
+
+ context 'when the project is forked and has a repository', :request_store do
+ let(:public_project) { create(:project, :public, :repository) }
+ let(:other_user) { create(:user) }
+
+ render_views
+
+ before do
+ # View the project as a user that does not have any rights
+ sign_in(other_user)
+
+ fork_project(public_project)
+ end
+
+ it 'does not increase the number of queries when the project is forked' do
+ expected_query = /#{public_project.fork_network.find_forks_in(other_user.namespace).to_sql}/
+
+ expect { get(:show, namespace_id: public_project.namespace, id: public_project) }
+ .not_to exceed_query_limit(1).for_query(expected_query)
+ end
+ end
end
describe "#update" do
@@ -405,11 +426,12 @@ describe ProjectsController do
end
end
- describe 'PUT #new_issue_address' do
+ describe 'PUT #new_issuable_address for issue' do
subject do
- put :new_issue_address,
+ put :new_issuable_address,
namespace_id: project.namespace,
- id: project
+ id: project,
+ issuable_type: 'issue'
user.reload
end
@@ -428,7 +450,35 @@ describe ProjectsController do
end
it 'changes projects new issue address' do
- expect { subject }.to change { project.new_issue_address(user) }
+ expect { subject }.to change { project.new_issuable_address(user, 'issue') }
+ end
+ end
+
+ describe 'PUT #new_issuable_address for merge request' do
+ subject do
+ put :new_issuable_address,
+ namespace_id: project.namespace,
+ id: project,
+ issuable_type: 'merge_request'
+ user.reload
+ end
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
+ end
+
+ it 'has http status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'changes the user incoming email token' do
+ expect { subject }.to change { user.incoming_email_token }
+ end
+
+ it 'changes projects new merge request address' do
+ expect { subject }.to change { project.new_issuable_address(user, 'merge_request') }
end
end
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index cf2a2b76bcb..860973024c9 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -4,5 +4,6 @@ FactoryGirl.define do
factory :appearance do
title "MepMep"
description "This is my Community Edition instance"
+ new_project_guidelines "Custom project guidelines"
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index cf38066dedc..c868525cbc0 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -154,36 +154,29 @@ FactoryGirl.define do
runner factory: :ci_runner
end
- trait :artifacts do
+ trait :legacy_artifacts do
after(:create) do |build, _|
- build.artifacts_file =
- fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
- 'application/zip')
-
- build.artifacts_metadata =
- fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
- 'application/x-gzip')
-
- build.save!
+ build.update!(
+ legacy_artifacts_file: fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip'),
+ legacy_artifacts_metadata: fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
+ )
end
end
- trait :artifacts_expired do
- after(:create) do |build, _|
- build.artifacts_file =
- fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
- 'application/zip')
-
- build.artifacts_metadata =
- fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
- 'application/x-gzip')
-
- build.artifacts_expire_at = 1.minute.ago
-
- build.save!
+ trait :artifacts do
+ after(:create) do |build|
+ create(:ci_job_artifact, :archive, job: build)
+ create(:ci_job_artifact, :metadata, job: build)
+ build.reload
end
end
+ trait :expired do
+ artifacts_expire_at 1.minute.ago
+ end
+
trait :with_commit do
after(:build) do |build|
allow(build).to receive(:commit).and_return build(:commit, :without_author)
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
new file mode 100644
index 00000000000..538dc422832
--- /dev/null
+++ b/spec/factories/ci/job_artifacts.rb
@@ -0,0 +1,30 @@
+include ActionDispatch::TestProcess
+
+FactoryGirl.define do
+ factory :ci_job_artifact, class: Ci::JobArtifact do
+ job factory: :ci_build
+ file_type :archive
+
+ after :build do |artifact|
+ artifact.project ||= artifact.job.project
+ end
+
+ trait :archive do
+ file_type :archive
+
+ after(:build) do |artifact, _|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ end
+ end
+
+ trait :metadata do
+ file_type :metadata
+
+ after(:build) do |artifact, _|
+ artifact.file = fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
+ end
+ end
+ end
+end
diff --git a/spec/factories/clusters/cluster.rb b/spec/factories/clusters/clusters.rb
index c4261178f2d..9e73a19e856 100644
--- a/spec/factories/clusters/cluster.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -13,27 +13,24 @@ FactoryGirl.define do
provider_type :user
platform_type :kubernetes
- platform_kubernetes do
- create(:cluster_platform_kubernetes, :configured)
- end
+ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
trait :provided_by_gcp do
provider_type :gcp
platform_type :kubernetes
- before(:create) do |cluster, evaluator|
- cluster.platform_kubernetes = build(:cluster_platform_kubernetes, :configured)
- cluster.provider_gcp = build(:cluster_provider_gcp, :created)
- end
+ provider_gcp factory: [:cluster_provider_gcp, :created]
+ platform_kubernetes factory: [:cluster_platform_kubernetes, :configured]
end
trait :providing_by_gcp do
provider_type :gcp
+ provider_gcp factory: [:cluster_provider_gcp, :creating]
+ end
- provider_gcp do
- create(:cluster_provider_gcp, :creating)
- end
+ trait :disabled do
+ enabled false
end
end
end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index f4f12a095fc..4e2d8e8969e 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -2,15 +2,28 @@ require_relative '../support/repo_helpers'
FactoryGirl.define do
factory :commit do
- git_commit RepoHelpers.sample_commit
+ transient do
+ author nil
+ end
+
+ git_commit do
+ commit = RepoHelpers.sample_commit
+
+ if author
+ commit.author_email = author.email
+ commit.author_name = author.name
+ end
+
+ commit
+ end
project
initialize_with do
new(git_commit, project)
end
- after(:build) do |commit|
- allow(commit).to receive(:author).and_return build(:author)
+ after(:build) do |commit, evaluator|
+ allow(commit).to receive(:author).and_return(evaluator.author || build(:author))
end
trait :without_author do
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index ab4ae123429..471bfb3213a 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -63,13 +63,19 @@ FactoryGirl.define do
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
association :project, :repository
+
+ transient do
+ line_number 14
+ diff_refs { project.commit(commit_id).try(:diff_refs) }
+ end
+
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
- new_line: 14,
- diff_refs: project.commit(commit_id).try(:diff_refs)
+ new_line: line_number,
+ diff_refs: diff_refs
)
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 3222c41c3d8..e18f1a6bd4a 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -4,5 +4,21 @@ FactoryGirl.define do
path { "uploads/-/system/project/avatar/avatar.jpg" }
size 100.kilobytes
uploader "AvatarUploader"
+
+ trait :personal_snippet do
+ model { build(:personal_snippet) }
+ uploader "PersonalFileUploader"
+ end
+
+ trait :issuable_upload do
+ path { "#{SecureRandom.hex}/myfile.jpg" }
+ uploader "FileUploader"
+ end
+
+ trait :namespace_upload do
+ path { "#{SecureRandom.hex}/myfile.jpg" }
+ model { build(:group) }
+ uploader "NamespaceFileUploader"
+ end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 4000cd085b7..8ace424f8af 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -58,6 +58,10 @@ FactoryGirl.define do
end
end
+ trait :readme do
+ project_view :readme
+ end
+
factory :omniauth_user do
transient do
extern_uid '123456'
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 5f3a37c1dcc..d91dcf76191 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -9,6 +9,7 @@ feature 'Admin Appearance' do
fill_in 'appearance_title', with: 'MyCompany'
fill_in 'appearance_description', with: 'dev server'
+ fill_in 'appearance_new_project_guidelines', with: 'Custom project guidelines'
click_button 'Save'
expect(current_path).to eq admin_appearances_path
@@ -16,21 +17,39 @@ feature 'Admin Appearance' do
expect(page).to have_field('appearance_title', with: 'MyCompany')
expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_field('appearance_new_project_guidelines', with: 'Custom project guidelines')
expect(page).to have_content 'Last edit'
end
- scenario 'Preview appearance' do
+ scenario 'Preview sign-in page appearance' do
sign_in(create(:admin))
visit admin_appearances_path
- click_link "Preview"
+ click_link "Sign-in page"
- expect_page_has_custom_appearance(appearance)
+ expect_custom_sign_in_appearance(appearance)
+ end
+
+ scenario 'Preview new project page appearance' do
+ sign_in(create(:admin))
+
+ visit admin_appearances_path
+ click_link "New project page"
+
+ expect_custom_new_project_appearance(appearance)
end
scenario 'Custom sign-in page' do
visit new_user_session_path
- expect_page_has_custom_appearance(appearance)
+
+ expect_custom_sign_in_appearance(appearance)
+ end
+
+ scenario 'Custom new project page' do
+ sign_in create(:user)
+ visit new_project_path
+
+ expect_custom_new_project_appearance(appearance)
end
scenario 'Appearance logo' do
@@ -57,11 +76,15 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(header_logo_selector)
end
- def expect_page_has_custom_appearance(appearance)
+ def expect_custom_sign_in_appearance(appearance)
expect(page).to have_content appearance.title
expect(page).to have_content appearance.description
end
+ def expect_custom_new_project_appearance(appearance)
+ expect(page).to have_content appearance.new_project_guidelines
+ end
+
def logo_selector
'//img[data-src^="/uploads/-/system/appearance/logo"]'
end
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 4430fc15501..ac3392b49f9 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check", :feature, :broken_storage do
+feature "Admin Health Check", :feature do
include StubENV
before do
@@ -36,6 +36,7 @@ feature "Admin Health Check", :feature, :broken_storage do
context 'when services are up' do
before do
+ stub_storage_settings({}) # Hide the broken storage
visit admin_health_check_path
end
@@ -56,10 +57,8 @@ feature "Admin Health Check", :feature, :broken_storage do
end
end
- context 'with repository storage failures' do
+ context 'with repository storage failures', :broken_storage do
before do
- # Track a failure
- Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil
visit admin_health_check_path
end
@@ -67,9 +66,10 @@ feature "Admin Health Check", :feature, :broken_storage do
hostname = Gitlab::Environment.hostname
maximum_failures = Gitlab::CurrentSettings.current_application_settings
.circuitbreaker_failure_count_threshold
+ number_of_failures = maximum_failures + 1
- expect(page).to have_content('broken: failed storage access attempt on host:')
- expect(page).to have_content("#{hostname}: 1 of #{maximum_failures} failures.")
+ expect(page).to have_content("broken: #{number_of_failures} failed storage access attempts:")
+ expect(page).to have_content("#{hostname}: #{number_of_failures} of #{maximum_failures} failures.")
end
it 'allows resetting storage failures' do
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index 4a7c3e4f1ab..7a395f62511 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -4,52 +4,74 @@ describe 'Auto deploy' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
- before do
- create :kubernetes_service, project: project
- project.team << [user, :master]
- sign_in user
- end
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'when no deployment service is active' do
+ before do
+ trun_off
+ end
- context 'when no deployment service is active' do
- before do
- project.kubernetes_service.update!(active: false)
+ it 'does not show a button to set up auto deploy' do
+ visit project_path(project)
+ expect(page).to have_no_content('Set up auto deploy')
+ end
end
- it 'does not show a button to set up auto deploy' do
- visit project_path(project)
- expect(page).to have_no_content('Set up auto deploy')
+ context 'when a deployment service is active' do
+ before do
+ trun_on
+ visit project_path(project)
+ end
+
+ it 'shows a button to set up auto deploy' do
+ expect(page).to have_link('Set up auto deploy')
+ end
+
+ it 'includes OpenShift as an available template', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+
+ within '.gitlab-ci-yml-selector' do
+ expect(page).to have_content('OpenShift')
+ end
+ end
+
+ it 'creates a merge request using "auto-deploy" branch', :js do
+ click_link 'Set up auto deploy'
+ click_button 'Apply a GitLab CI Yaml template'
+ within '.gitlab-ci-yml-selector' do
+ click_on 'OpenShift'
+ end
+ wait_for_requests
+ click_button 'Commit changes'
+
+ expect(page).to have_content('New Merge Request From auto-deploy into master')
+ end
end
end
- context 'when a deployment service is active' do
+ context 'when user configured kubernetes from Integration > Kubernetes' do
before do
- project.kubernetes_service.update!(active: true)
- visit project_path(project)
+ create :kubernetes_service, project: project
+ project.team << [user, :master]
+ sign_in user
end
- it 'shows a button to set up auto deploy' do
- expect(page).to have_link('Set up auto deploy')
- end
+ let(:trun_on) { project.deployment_platform.update!(active: true) }
+ let(:trun_off) { project.deployment_platform.update!(active: false) }
- it 'includes OpenShift as an available template', :js do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
- within '.gitlab-ci-yml-selector' do
- expect(page).to have_content('OpenShift')
- end
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ before do
+ create(:cluster, :provided_by_gcp, projects: [project])
+ project.team << [user, :master]
+ sign_in user
end
- it 'creates a merge request using "auto-deploy" branch', :js do
- click_link 'Set up auto deploy'
- click_button 'Apply a GitLab CI Yaml template'
- within '.gitlab-ci-yml-selector' do
- click_on 'OpenShift'
- end
- wait_for_requests
- click_button 'Commit changes'
+ let(:trun_on) { project.deployment_platform.cluster.update!(enabled: true) }
+ let(:trun_off) { project.deployment_platform.cluster.update!(enabled: false) }
- expect(page).to have_content('New Merge Request From auto-deploy into master')
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 98586ddbd81..77dcdf89f37 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -89,7 +89,7 @@ describe 'Commits' do
context 'Download artifacts' do
before do
- build.update_attributes(artifacts_file: artifacts_file)
+ build.update_attributes(legacy_artifacts_file: artifacts_file)
end
it do
@@ -146,7 +146,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
project.team << [user, :reporter]
- build.update_attributes(artifacts_file: artifacts_file)
+ build.update_attributes(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -168,7 +168,7 @@ describe 'Commits' do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
- build.update_attributes(artifacts_file: artifacts_file)
+ build.update_attributes(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -197,7 +197,7 @@ describe 'Commits' do
commits = project.repository.commits(branch_name)
commits.each do |commit|
- expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}")
+ expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}")
end
end
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 6f916078b1a..94133c62b5c 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -13,7 +13,7 @@ feature 'Dashboard Todos' do
end
it 'shows "All done" message' do
- expect(page).to have_content 'Todos let you see what you should do next.'
+ expect(page).to have_content 'Todos let you see what you should do next'
end
end
diff --git a/spec/features/groups/labels/user_sees_links_to_issuables.rb b/spec/features/groups/labels/user_sees_links_to_issuables.rb
new file mode 100644
index 00000000000..5d6290d2109
--- /dev/null
+++ b/spec/features/groups/labels/user_sees_links_to_issuables.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+feature 'Groups > Labels > User sees links to issuables' do
+ set(:group) { create(:group, :public) }
+
+ before do
+ create(:group_label, group: group, title: 'bug')
+ visit group_labels_path(group)
+ end
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+end
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb
index da1e17225db..21f7b4999ad 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members.rb
@@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do
end
end
+ scenario 'do not disclose email addresses', :js do
+ group.add_owner(user1)
+ create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe")
+
+ visit group_group_members_path(group)
+
+ find('.select2-container').click
+ select_input = find('.select2-input')
+
+ select_input.send_keys('@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content('No matches found')
+
+ select_input.native.clear
+ select_input.send_keys('undisclosed_email@gitlab.com')
+ wait_for_requests
+
+ expect(page).to have_content("Jane 'invisible' Doe")
+ end
+
scenario 'remove user from group', :js do
group.add_owner(user1)
group.add_developer(user2)
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
index 7ea29ff252b..ecbe51a7bc2 100644
--- a/spec/features/issuables/discussion_lock_spec.rb
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -14,7 +14,7 @@ describe 'Discussion Lock', :js do
project.add_developer(user)
end
- context 'when the discussion is unlocked' do
+ context 'when the discussion is unlocked' do
it 'the user can lock the issue' do
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 95d637265e0..6a9a80235c1 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -15,7 +15,7 @@ feature 'GFM autocomplete', :js do
end
it 'updates issue descripton with GFM reference' do
- find('.issuable-edit').click
+ find('.js-issuable-edit').click
simulate_input('#issue-description', "@#{user.name[0...3]}")
@@ -220,6 +220,89 @@ feature 'GFM autocomplete', :js do
end
end
+ # This context has jsut one example in each contexts in order to improve spec performance.
+ context 'labels' do
+ let!(:backend) { create(:label, project: project, title: 'backend') }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') }
+
+ context 'when no labels are assigned' do
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show no labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(not_shown: [backend, bug, feature_proposal])
+ end
+ end
+
+ context 'when some labels are assigned' do
+ before do
+ issue.labels << [backend]
+ end
+
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show only unset labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(shown: [bug, feature_proposal], not_shown: [backend])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show only set labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(shown: [backend], not_shown: [bug, feature_proposal])
+ end
+ end
+
+ context 'when all labels are assigned' do
+ before do
+ issue.labels << [backend, bug, feature_proposal]
+ end
+
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show no labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(not_shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+ end
+ end
+ end
+
+ private
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
@@ -232,4 +315,27 @@ feature 'GFM autocomplete', :js do
expect(note.value).not_to include("\"#{value}\"")
end
end
+
+ def expect_labels(shown: nil, not_shown: nil)
+ page.within('.atwho-container') do
+ if shown
+ expect(page).to have_selector('.atwho-view li', count: shown.size)
+ shown.each { |label| expect(page).to have_content(label.title) }
+ end
+
+ if not_shown
+ expect(page).not_to have_selector('.atwho-view li') unless shown
+ not_shown.each { |label| expect(page).not_to have_content(label.title) }
+ end
+ end
+ end
+
+ # `note` is a textarea where the given text should be typed.
+ # We don't want to find it each time this function gets called.
+ def type(note, text)
+ page.within('.timeline-content-form') do
+ note.set('')
+ note.native.send_keys(text)
+ end
+ end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index b9af77f918a..852d9e368aa 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -365,16 +365,16 @@ describe 'Issues' do
end
it 'changes incoming email address token', :js do
- find('.issue-email-modal-btn').click
- previous_token = find('input#issue_email').value
+ find('.issuable-email-modal-btn').click
+ previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
- expect(page).to have_no_field('issue_email', with: previous_token)
- new_token = project1.new_issue_address(user.reload)
+ expect(page).to have_no_field('issuable_email', with: previous_token)
+ new_token = project1.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
- 'issue_email',
+ 'issuable_email',
with: new_token
)
end
@@ -630,8 +630,8 @@ describe 'Issues' do
end
it 'click the button to show modal for the new email' do
- page.within '#issue-email-modal' do
- email = project.new_issue_address(user)
+ page.within '#issuable-email-modal' do
+ email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
end
diff --git a/spec/features/logout_spec.rb b/spec/features/logout_spec.rb
new file mode 100644
index 00000000000..635729efa53
--- /dev/null
+++ b/spec/features/logout_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe 'Logout/Sign out', :js do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ visit root_path
+ end
+
+ it 'sign out redirects to sign in page' do
+ gitlab_sign_out
+
+ expect(current_path).to eq new_user_session_path
+ end
+
+ it 'sign out does not show signed out flash notice' do
+ gitlab_sign_out
+
+ expect(page).not_to have_selector('.flash-notice')
+ end
+end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index cc1b187ff54..e285befc66f 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -207,8 +207,9 @@ describe 'GitLab Markdown' do
before do
@feat = MarkdownFeature.new
- # `markdown` helper expects a `@project` variable
+ # `markdown` helper expects a `@project` and `@group` variable
@project = @feat.project
+ @group = @feat.group
end
context 'default pipeline' do
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb
index 3c53b51e330..021c4e03428 100644
--- a/spec/features/merge_requests/image_diff_notes.rb
+++ b/spec/features/merge_requests/image_diff_notes.rb
@@ -185,6 +185,18 @@ feature 'image diff notes', :js do
expect(page).to have_content(diff_note.note)
end
end
+
+ describe 'image view modes' do
+ before do
+ visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ end
+
+ it 'resizes image in onion skin view mode' do
+ find('.view-modes-menu .onion-skin').click
+
+ expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ end
+ end
end
def create_image_diff_note
diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
index bac56270362..93c5e945453 100644
--- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb
@@ -28,14 +28,14 @@ feature 'Mini Pipeline Graph', :js do
let(:artifacts_file2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') }
before do
- create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file1)
+ create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
create(:ci_build, pipeline: pipeline, when: 'manual')
end
it 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) }
- create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file2)
+ create(:ci_build, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
create(:ci_build, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) }
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index a3fcc27cab0..307c860eac4 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do
end
before do
- visit project_merge_request_path(project, merge_request)
+ merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
scenario 'user visits merge request pipelines tab' do
+ visit project_merge_request_path(project, merge_request)
+
+ expect(page.find('.ci-widget')).to have_content('pending')
+
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do
expect(page).to have_selector('.stage-cell')
end
+
+ scenario 'pipeline sha does not equal last commit sha' do
+ pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page.find('.ci-widget')).to have_content(
+ 'Could not connect to the CI server. Please check your settings and try again')
+ end
end
context 'without pipelines' do
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 29f95039af8..482f2e51c8b 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -6,18 +6,47 @@ feature 'Merge Request versions', :js do
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let!(:params) { Hash.new }
before do
sign_in(create(:admin))
- visit diffs_project_merge_request_path(project, merge_request)
+ visit diffs_project_merge_request_path(project, merge_request, params)
end
- it 'show the latest version of the diff' do
- page.within '.mr-version-dropdown' do
- expect(page).to have_content 'latest version'
+ shared_examples 'allows commenting' do |file_id:, line_code:, comment:|
+ it do
+ diff_file_selector = ".diff-file[id='#{file_id}']"
+ line_code = "#{file_id}_#{line_code}"
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: comment
+ find(".js-comment-button").click
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content(comment)
+ end
end
+ end
- expect(page).to have_content '8 changed files'
+ describe 'compare with the latest version' do
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '1_1',
+ comment: 'Typo, please fix.'
end
describe 'switch between versions' do
@@ -62,24 +91,10 @@ feature 'Merge Request versions', :js do
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
- it 'allows commenting' do
- diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
- line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
-
- page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
- find(".line_holder[id='#{line_code}'] button").click
-
- page.within("form[data-line-code='#{line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
-
- wait_for_requests
-
- expect(page).to have_content("Typo, please fix")
- end
- end
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '2_2',
+ comment: 'Typo, please fix.'
end
describe 'compare with older version' do
@@ -132,25 +147,6 @@ feature 'Merge Request versions', :js do
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
- it 'allows commenting' do
- diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
- line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
-
- page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
- find(".line_holder[id='#{line_code}'] button").click
-
- page.within("form[data-line-code='#{line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
-
- wait_for_requests
-
- expect(page).to have_content("Typo, please fix")
- end
- end
-
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
@@ -162,6 +158,11 @@ feature 'Merge Request versions', :js do
end
expect(page).to have_content '8 changed files'
end
+
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '4_4',
+ comment: 'Typo, please fix.'
end
describe 'compare with same version' do
@@ -210,4 +211,24 @@ feature 'Merge Request versions', :js do
expect(page).to have_content '0 changed files'
end
end
+
+ describe 'scoped in a commit' do
+ let(:params) { { commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } }
+
+ before do
+ wait_for_requests
+ end
+
+ it 'should only show diffs from the commit' do
+ diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']}
+
+ expect(diff_commit_ids).not_to be_empty
+ expect(diff_commit_ids).to all(eq(params[:commit_id]))
+ end
+
+ it_behaves_like 'allows commenting',
+ file_id: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd',
+ line_code: '6_6',
+ comment: 'Typo, please fix.'
+ end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 2bad3b02250..3ee094c216e 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -63,6 +63,18 @@ describe 'Merge request', :js do
expect(page).to have_selector('.accept-merge-request')
expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
+
+ it 'allows me to merge, see cherry-pick modal and load branches list' do
+ wait_for_requests
+ click_button 'Merge'
+
+ wait_for_requests
+ click_link 'Cherry-pick'
+ page.find('.js-project-refs-dropdown').click
+ wait_for_requests
+
+ expect(page.all('.js-cherry-pick-form .dropdown-content li').size).to be > 1
+ end
end
context 'view merge request with external CI service' do
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c60883911f7..0848857ed1e 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -25,7 +25,7 @@ describe 'Profile account page', :js do
fill_in 'password', with: '12345678'
- page.within '.popup-dialog' do
+ page.within '.modal' do
click_button 'Delete account'
end
@@ -38,7 +38,7 @@ describe 'Profile account page', :js do
fill_in 'password', with: 'testing123'
- page.within '.popup-dialog' do
+ page.within '.modal' do
click_button 'Delete account'
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
new file mode 100644
index 00000000000..b34cd061ec6
--- /dev/null
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+feature 'Clusters Applications', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ describe 'Installing applications' do
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ context 'when cluster is being created' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project])}
+
+ scenario 'user is unable to install applications' do
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ end
+ end
+ end
+
+ context 'when cluster is created' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])}
+
+ scenario 'user can install applications' do
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ end
+ end
+
+ context 'when user installs Helm' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'he sees status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user installs Ingress' do
+ context 'when user installs application: Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+
+ page.within('.js-cluster-application-row-ingress') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'he sees status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your cluster')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
new file mode 100644
index 00000000000..67b8901f8fb
--- /dev/null
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+feature 'Gcp Cluster', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ end
+
+ context 'when user has signed with Google' do
+ before do
+ allow_any_instance_of(Projects::Clusters::GcpController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::Clusters::GcpController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+
+ Clusters::Cluster.last.provider.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine')
+ end
+
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).to have_content('Something wrong!')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ click_button 'Save changes'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_link('Add cluster')
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed with Google' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
+ end
+
+ it 'user sees a login page' do
+ expect(page).to have_css('.signin-with-google')
+ end
+ end
+end
diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb
new file mode 100644
index 00000000000..01f9526608f
--- /dev/null
+++ b/spec/features/projects/clusters/interchangeability_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do
+ EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze
+ EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze
+
+ it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do
+ expected_interfaces = KubernetesService.instance_methods(false)
+ expected_interfaces = expected_interfaces - EXCEPT_METHODS
+ EXCEPT_METHODS_GREP_V.each do |g|
+ expected_interfaces = expected_interfaces.grep_v(/#{Regexp.escape(g)}\z/)
+ end
+
+ expect(expected_interfaces - Clusters::Platforms::Kubernetes.instance_methods).to be_empty
+ end
+end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
new file mode 100644
index 00000000000..414f4acba86
--- /dev/null
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+feature 'User Cluster', :js do
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ gitlab_sign_in(user)
+ allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add cluster'
+ click_link 'Add an existing cluster'
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ fill_in 'cluster_name', with: 'dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
+ fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
+ click_button 'Add cluster'
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_content('Enable cluster integration')
+ expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
+ .to have_content('http://example.com')
+ expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
+ .to have_content('my-token')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Add cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+
+ before do
+ visit project_cluster_path(project, cluster)
+ end
+
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save')
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ fill_in 'cluster_name', with: 'dev-cluster'
+ click_button 'Save'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_name', with: 'my-dev-cluster'
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ click_button 'Save changes'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ expect(cluster.reload.name).to eq('my-dev-cluster')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_link('Add cluster')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 197e6df4997..93929bf6814 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -3,204 +3,78 @@ require 'spec_helper'
feature 'Clusters', :js do
include GoogleApi::CloudPlatformHelpers
- let!(:project) { create(:project, :repository) }
- let!(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
before do
project.add_master(user)
gitlab_sign_in(user)
end
- context 'when user has signed in Google' do
+ context 'when user does not have a cluster and visits cluster index page' do
before do
- allow_any_instance_of(Projects::ClustersController)
- .to receive(:token_in_session).and_return('token')
- allow_any_instance_of(Projects::ClustersController)
- .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ visit project_clusters_path(project)
end
- context 'when user does not have a cluster and visits cluster index page' do
- before do
- visit project_clusters_path(project)
-
- click_link 'Create on GKE'
- end
-
- it 'user sees a new page' do
- expect(page).to have_button('Create cluster')
- end
-
- context 'when user filled form with valid parameters' do
- before do
- double.tap do |dbl|
- allow(dbl).to receive(:status).and_return('RUNNING')
- allow(dbl).to receive(:self_link)
- .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create).and_return(dbl)
- end
-
- allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
-
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create cluster'
- end
-
- it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
+ it 'sees empty state' do
+ expect(page).to have_link('Add cluster')
+ expect(page).to have_selector('.empty-state')
+ end
+ end
- # Application Installation buttons
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
- end
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- Clusters::Cluster.last.provider.make_created!
+ before do
+ visit project_clusters_path(project)
+ end
- expect(page).to have_content('Cluster was successfully created on Google Container Engine')
- end
+ it 'user sees a table with one cluster' do
+ # One is the header row, the other the cluster row
+ expect(page).to have_selector('.gl-responsive-table-row', count: 2)
+ end
- it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
+ context 'inline update of cluster' do
+ it 'user can update cluster' do
+ expect(page).to have_selector('.js-toggle-cluster-list')
+ end
- Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+ context 'with sucessfull request' do
+ it 'user sees updated cluster' do
+ expect do
+ page.find('.js-toggle-cluster-list').click
+ wait_for_requests
+ end.to change { cluster.reload.enabled }
- expect(page).to have_content('Something wrong!')
+ expect(page).not_to have_selector('.is-checked')
+ expect(cluster.reload).not_to be_enabled
end
end
- context 'when user filled form with invalid parameters' do
- before do
- click_button 'Create cluster'
- end
+ context 'with failed request' do
+ it 'user sees not update cluster and error message' do
+ expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
+ allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
- it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ page.find('.js-toggle-cluster-list').click
+
+ expect(page).to have_content('Something went wrong on our end.')
+ expect(page).to have_selector('.is-checked')
+ expect(cluster.reload).to be_enabled
end
end
end
- context 'when user has a cluster and visits cluster index page' do
- let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
+ context 'when user clicks on a cluster' do
before do
- visit project_clusters_path(project)
+ click_link cluster.name
end
- it 'user sees an cluster details page' do
+ it 'user sees a cluster details page' do
expect(page).to have_button('Save')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
-
- # Application Installation buttons
- page.within('.js-cluster-application-row-helm') do
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
- end
end
-
- context 'when user installs application: Helm Tiller' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
-
- page.within('.js-cluster-application-row-helm') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
- end
-
- it 'user sees status transition' do
- page.within('.js-cluster-application-row-helm') do
- # FE sends request and gets the response, then the buttons is "Install"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
-
- Clusters::Cluster.last.application_helm.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
-
- Clusters::Cluster.last.application_helm.make_installed!
-
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
- end
-
- expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
- end
- end
-
- context 'when user installs application: Ingress' do
- before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
- # Helm Tiller needs to be installed before you can install Ingress
- create(:cluster_applications_helm, :installed, cluster: cluster)
-
- visit project_clusters_path(project)
-
- page.within('.js-cluster-application-row-ingress') do
- page.find(:css, '.js-cluster-application-install-button').click
- end
- end
-
- it 'user sees status transition' do
- page.within('.js-cluster-application-row-ingress') do
- # FE sends request and gets the response, then the buttons is "Install"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
-
- Clusters::Cluster.last.application_ingress.make_installing!
-
- # FE starts polling and update the buttons to "Installing"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
-
- Clusters::Cluster.last.application_ingress.make_installed!
-
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
- end
-
- expect(page).to have_content('Ingress was successfully installed on your cluster')
- end
- end
-
- context 'when user disables the cluster' do
- before do
- page.find(:css, '.js-toggle-cluster').click
- click_button 'Save'
- end
-
- it 'user sees the succeccful message' do
- expect(page).to have_content('Cluster was successfully updated.')
- end
- end
-
- context 'when user destory the cluster' do
- before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
- end
-
- it 'user sees creation form with the succeccful message' do
- expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Create on GKE')
- end
- end
- end
- end
-
- context 'when user has not signed in Google' do
- before do
- visit project_clusters_path(project)
-
- click_link 'Create on GKE'
- end
-
- it 'user sees a login page' do
- expect(page).to have_css('.signin-with-google')
end
end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 5fc3ba54f65..dfcf97ad495 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -101,35 +101,48 @@ feature 'Environment' do
end
context 'with terminal' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ scenario 'it shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
- scenario 'it shows the terminal button' do
- expect(page).to have_terminal_button
+ context 'web terminal', :js do
+ before do
+ # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
+ allow_any_instance_of(Environment).to receive(:terminals) { nil }
+ visit terminal_project_environment_path(project, environment)
+ end
+
+ it 'displays a web terminal' do
+ expect(page).to have_selector('#terminal')
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
- context 'web terminal', :js do
- before do
- # Stub #terminals as it causes js-enabled feature specs to render the page incorrectly
- allow_any_instance_of(Environment).to receive(:terminals) { nil }
- visit terminal_project_environment_path(project, environment)
- end
+ context 'for developer' do
+ let(:role) { :developer }
- it 'displays a web terminal' do
- expect(page).to have_selector('#terminal')
- expect(page).to have_link(nil, href: environment.external_url)
+ scenario 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
end
end
end
- context 'for developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- scenario 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 879ee6f4b9b..4a05313c14a 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -208,22 +208,35 @@ feature 'Environments page', :js do
end
context 'when kubernetes terminal is available' do
- let(:project) { create(:kubernetes_project, :test_repo) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'for project master' do
+ let(:role) { :master }
- context 'for project master' do
- let(:role) { :master }
+ it 'shows the terminal button' do
+ expect(page).to have_terminal_button
+ end
+ end
+
+ context 'when user is a developer' do
+ let(:role) { :developer }
- it 'shows the terminal button' do
- expect(page).to have_terminal_button
+ it 'does not show terminal button' do
+ expect(page).not_to have_terminal_button
+ end
end
end
- context 'when user is a developer' do
- let(:role) { :developer }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project, :test_repo) }
- it 'does not show terminal button' do
- expect(page).not_to have_terminal_button
- end
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [create(:project, :repository)]) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 951456763dc..033c45a60bf 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2)
+ expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2)
end
it "shows empty features project homepage" do
@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.is-checked)").click
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index a012db8fd27..0257cd157c9 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -32,7 +32,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.content .issuable-actions') do
+ page.within('.js-issuable-actions') do
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title'
@@ -77,7 +77,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.content .issuable-actions') do
+ page.within('.js-issuable-actions') do
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title'
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index c2a0d2395a9..0b0d5a2dce8 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -187,7 +187,7 @@ feature 'Jobs' do
context "Download artifacts" do
before do
- job.update_attributes(artifacts_file: artifacts_file)
+ job.update_attributes(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
end
@@ -198,7 +198,7 @@ feature 'Jobs' do
context 'Artifacts expire date' do
before do
- job.update_attributes(artifacts_file: artifacts_file,
+ job.update_attributes(legacy_artifacts_file: artifacts_file,
artifacts_expire_at: expire_at)
visit project_job_path(project, job)
@@ -422,14 +422,14 @@ feature 'Jobs' do
describe "GET /:project/jobs/:id/download" do
before do
- job.update_attributes(artifacts_file: artifacts_file)
+ job.update_attributes(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
click_link 'Download'
end
context "Build from other project" do
before do
- job2.update_attributes(artifacts_file: artifacts_file)
+ job2.update_attributes(legacy_artifacts_file: artifacts_file)
visit download_project_job_artifacts_path(project, job2)
end
diff --git a/spec/features/projects/labels/user_sees_links_to_issuables.rb b/spec/features/projects/labels/user_sees_links_to_issuables.rb
new file mode 100644
index 00000000000..aa56fd7f74e
--- /dev/null
+++ b/spec/features/projects/labels/user_sees_links_to_issuables.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+feature 'Projects > Labels > User sees links to issuables' do
+ set(:user) { create(:user) }
+
+ before do
+ label # creates the label
+ project.add_developer(user)
+ sign_in user
+ visit project_labels_path(project)
+ end
+
+ context 'with a project label' do
+ let(:label) { create(:label, project: project, title: 'bug') }
+
+ context 'when merge requests and issues are enabled for the project' do
+ let(:project) { create(:project, :public) }
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ context 'when issues are disabled for the project' do
+ let(:project) { create(:project, :public, issues_access_level: ProjectFeature::DISABLED) }
+
+ scenario 'shows links to MRs but not to issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).not_to have_link('view open issues')
+ end
+ end
+
+ context 'when merge requests are disabled for the project' do
+ let(:project) { create(:project, :public, merge_requests_access_level: ProjectFeature::DISABLED) }
+
+ scenario 'shows links to issues but not to MRs' do
+ expect(page).not_to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+ end
+
+ context 'with a group label' do
+ set(:group) { create(:group) }
+ let(:label) { create(:group_label, group: group, title: 'bug') }
+
+ context 'when merge requests and issues are enabled for the project' do
+ let(:project) { create(:project, :public, namespace: group) }
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ context 'when issues are disabled for the project' do
+ let(:project) { create(:project, :public, namespace: group, issues_access_level: ProjectFeature::DISABLED) }
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+
+ context 'when merge requests are disabled for the project' do
+ let(:project) { create(:project, :public, namespace: group, merge_requests_access_level: ProjectFeature::DISABLED) }
+
+ scenario 'shows links to MRs and issues' do
+ expect(page).to have_link('view merge requests')
+ expect(page).to have_link('view open issues')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index b8fa1a54c24..888e290292b 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -185,6 +185,36 @@ describe 'Pipeline', :js do
end
end
+ context 'when user does not have access to read jobs' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ describe 'GET /:project/pipelines/:id' do
+ include_context 'pipeline builds'
+
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
+
+ before do
+ visit project_pipeline_path(project, pipeline)
+ end
+
+ it 'shows the pipeline graph' do
+ expect(page).to have_selector('.pipeline-visualization')
+ expect(page).to have_content('Build')
+ expect(page).to have_content('Test')
+ expect(page).to have_content('Deploy')
+ expect(page).to have_content('Retry')
+ expect(page).to have_content('Cancel running')
+ end
+
+ it 'should not link to job' do
+ expect(page).not_to have_selector('.js-pipeline-graph-job-link')
+ end
+ end
+ end
+
describe 'GET /:project/pipelines/:id/builds' do
include_context 'pipeline builds'
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index a1b1d94ae06..b87b47d0e1a 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -304,7 +304,7 @@ describe 'Pipelines', :js do
context 'with artifacts expired' do
let!(:with_artifacts_expired) do
- create(:ci_build, :artifacts_expired, :success,
+ create(:ci_build, :expired, :success,
pipeline: pipeline,
name: 'rspec',
stage: 'test')
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index eb8e7265dd3..561f08cba00 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -59,107 +59,6 @@ feature "Pipelines settings" do
expect(project.auto_devops).to be_present
expect(project.auto_devops).not_to be_enabled
end
-
- describe 'Immediately run pipeline checkbox option', :js do
- context 'when auto devops is set to instance default (enabled)' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: nil)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit disabled hides all checkboxes' do
- page.choose('project_auto_devops_attributes_enabled_false')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit enabled hides all checkboxes because we are already enabled' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
- end
-
- context 'when auto devops is set to instance default (disabled)' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- project.create_auto_devops!(enabled: nil)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit disabled hides all checkboxes' do
- page.choose('project_auto_devops_attributes_enabled_false')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit enabled shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
- end
-
- context 'when auto devops is set to explicit disabled' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: false)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 2, visible: false)
- end
-
- it 'selecting explicit enabled shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
-
- it 'selecting instance default (enabled) shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
- end
-
- context 'when auto devops is set to explicit enabled' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- project.create_auto_devops!(enabled: true)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not have any checkboxes' do
- expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
- end
- end
-
- context 'when master contains a .gitlab-ci.yml file' do
- let(:project) { create(:project, :repository) }
-
- before do
- project.repository.create_file(user, '.gitlab-ci.yml', "script: ['test']", message: 'test', branch_name: project.default_branch)
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: false)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not have any checkboxes' do
- expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
- end
- end
- end
end
end
end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index 1cfbbb4cb62..0fa7ca9afd4 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -39,6 +39,11 @@ describe 'Project snippets', :js do
expect(page).to have_selector('.atwho-view')
end
+
+ it 'should have zen mode' do
+ find('.js-zen-enter').click()
+ expect(page).to have_selector('.fullscreen')
+ end
end
end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 156293289dd..8f06328962e 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -20,7 +20,7 @@ feature 'Multi-file editor new directory', :js do
click_link('New directory')
- page.within('.popup-dialog') do
+ page.within('.modal') do
find('.form-control').set('foldername')
click_button('Create directory')
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 8fb8476e631..bdebc12ef47 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -20,7 +20,7 @@ feature 'Multi-file editor new file', :js do
click_link('New file')
- page.within('.popup-dialog') do
+ page.within('.modal') do
find('.form-control').set('filename')
click_button('Create file')
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
new file mode 100644
index 00000000000..c8a17871508
--- /dev/null
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+feature 'Projects tree' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit project_tree_path(project, 'master')
+ end
+
+ it 'renders tree table' do
+ expect(page).to have_selector('.tree-item')
+ expect(page).not_to have_selector('.label-lfs', text: 'LFS')
+ end
+
+ context 'LFS' do
+ before do
+ visit project_tree_path(project, File.join('master', 'files/lfs'))
+ end
+
+ it 'renders LFS badge on blob item' do
+ expect(page).to have_selector('.label-lfs', text: 'LFS')
+ end
+ end
+end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
new file mode 100644
index 00000000000..c10efac2432
--- /dev/null
+++ b/spec/finders/clusters_finder_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe ClustersFinder do
+ let(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ describe '#execute' do
+ let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+
+ subject { described_class.new(project, user, scope).execute }
+
+ context 'when scope is all' do
+ let(:scope) { :all }
+
+ it { is_expected.to match_array([enabled_cluster, disabled_cluster]) }
+ end
+
+ context 'when scope is active' do
+ let(:scope) { :active }
+
+ it { is_expected.to match_array([enabled_cluster]) }
+ end
+
+ context 'when scope is inactive' do
+ let(:scope) { :inactive }
+
+ it { is_expected.to match_array([disabled_cluster]) }
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/contributor.json b/spec/fixtures/api/schemas/contributor.json
new file mode 100644
index 00000000000..e88470a2363
--- /dev/null
+++ b/spec/fixtures/api/schemas/contributor.json
@@ -0,0 +1,18 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "email",
+ "commits",
+ "additions",
+ "deletions"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "email": { "type": "string" },
+ "commits": { "type": "integer" },
+ "additions": { "type": "integer" },
+ "deletions": { "type": "integer" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/contributors.json b/spec/fixtures/api/schemas/contributors.json
new file mode 100644
index 00000000000..a9f1d1ea64f
--- /dev/null
+++ b/spec/fixtures/api/schemas/contributors.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "contributor.json" }
+}
diff --git a/spec/fixtures/emails/valid_new_merge_request.eml b/spec/fixtures/emails/valid_new_merge_request.eml
new file mode 100644
index 00000000000..729df674604
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request.eml
@@ -0,0 +1,20 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: feature
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Merge request description
diff --git a/spec/fixtures/emails/valid_new_merge_request_no_description.eml b/spec/fixtures/emails/valid_new_merge_request_no_description.eml
new file mode 100644
index 00000000000..480675a6d7e
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request_no_description.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: feature
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/valid_new_merge_request_no_subject.eml b/spec/fixtures/emails/valid_new_merge_request_no_subject.eml
new file mode 100644
index 00000000000..27eb1b7d922
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_merge_request_no_subject.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject:
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 638cd8b07c8..71abb6da607 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -258,12 +258,23 @@ With inline diffs tags you can display {+ additions +} or [- deletions -].
The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
-However the wrapping tags can not be mixed as such -
+Examples:
+```
+- {+ additions +}
+- [+ additions +]
+- {- deletions -}
+- [- deletions -]
+```
+
+However the wrapping tags cannot be mixed as such:
+
+```
- {+ additions +]
- [+ additions +}
-- {- delletions -]
-- [- delletions -}
+- {- deletions -]
+- [- deletions -}
+```
### Videos
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 7266e1b84d1..5e272af6073 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -82,104 +82,4 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
-
- describe '.show_run_auto_devops_pipeline_checkbox_for_instance_setting?' do
- subject { helper.show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) }
-
- context 'when master contains a .gitlab-ci.yml file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly enabled' do
- before do
- project.create_auto_devops!(enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly disabled' do
- before do
- project.create_auto_devops!(enabled: false)
- end
-
- context 'when auto devops is enabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when auto devops is disabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when auto devops is set to instance setting' do
- before do
- project.create_auto_devops!(enabled: nil)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- describe '.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?' do
- subject { helper.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) }
-
- context 'when master contains a .gitlab-ci.yml file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly enabled' do
- before do
- project.create_auto_devops!(enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly disabled' do
- before do
- project.create_auto_devops!(enabled: false)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when auto devops is set to instance setting' do
- before do
- project.create_auto_devops!(enabled: nil)
- end
-
- context 'when auto devops is enabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is disabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it { is_expected.to eq(true) }
- end
- end
- end
end
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
new file mode 100644
index 00000000000..a3c5ab99c87
--- /dev/null
+++ b/spec/helpers/boards_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe BoardsHelper do
+ describe '#board_data' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+
+ before do
+ assign(:board, board)
+ assign(:project, project)
+
+ allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).with(user, :admin_list, project).and_return(true)
+ end
+
+ it 'returns a board_lists_path as lists_endpoint' do
+ expect(helper.board_data[:lists_endpoint]).to eq(board_lists_path(board))
+ end
+ end
+end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index e5158761333..fee8df10129 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -26,9 +26,10 @@ describe ButtonHelper do
context 'when user has password automatically set' do
let(:user) { create(:user, password_automatically_set: true) }
- it 'shows a password tooltip' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Set a password on your account to pull or push via HTTP.')
+ it 'shows the password text on the dropdown' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq 'Set a password on your account to pull or push via HTTP.'
end
end
end
@@ -39,17 +40,10 @@ describe ButtonHelper do
end
context 'when user has no personal access tokens' do
- it 'has a personal access token tooltip ' do
- expect(element.attr('class')).to include(has_tooltip_class)
- expect(element.attr('data-title')).to eq('Create a personal access token on your account to pull or push via HTTP.')
- end
- end
+ it 'has a personal access token text on the dropdown description ' do
+ description = element.search('.dropdown-menu-inner-content').first
- context 'when user has a personal access token' do
- it 'shows no tooltip' do
- create(:personal_access_token, user: user)
-
- expect(element.attr('class')).not_to include(has_tooltip_class)
+ expect(description.inner_text).to eq 'Create a personal access token on your account to pull or push via HTTP.'
end
end
end
@@ -63,6 +57,69 @@ describe ButtonHelper do
end
end
+ describe 'ssh_button' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def element
+ element = helper.ssh_clone_button(project)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'without an ssh key on the user' do
+ it 'shows a warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description.inner_text).to eq "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+ end
+ end
+
+ context 'with an ssh key on the user' do
+ before do
+ create(:key, user: user)
+ end
+
+ it 'there is no warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description).to eq nil
+ end
+ end
+ end
+
+ describe 'ssh and http clone buttons' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def http_button_element
+ element = helper.http_clone_button(project, append_link: false)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ def ssh_button_element
+ element = helper.ssh_clone_button(project, append_link: false)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'only shows the title of any of the clone buttons when append_link is false' do
+ expect(http_button_element.text).to eq('HTTP')
+ expect(http_button_element.search('.dropdown-menu-inner-content').first).to eq(nil)
+ expect(ssh_button_element.text).to eq('SSH')
+ expect(ssh_button_element.search('.dropdown-menu-inner-content').first).to eq(nil)
+ end
+ end
+
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 4ac4302adfd..0286d36952c 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -1,6 +1,69 @@
require 'spec_helper'
describe LabelsHelper do
+ describe '#show_label_issuables_link?' do
+ shared_examples 'a valid response to show_label_issuables_link?' do |issuables_type, when_enabled = true, when_disabled = false|
+ let(:context_project) { project }
+
+ context "when asking for a #{issuables_type} link" do
+ subject { show_label_issuables_link?(label, issuables_type, project: context_project) }
+
+ context "when #{issuables_type} are enabled for the project" do
+ let(:project) { create(:project, "#{issuables_type}_access_level": ProjectFeature::ENABLED) }
+
+ it { is_expected.to be(when_enabled) }
+ end
+
+ context "when #{issuables_type} are disabled for the project" do
+ let(:project) { create(:project, :public, "#{issuables_type}_access_level": ProjectFeature::DISABLED) }
+
+ it { is_expected.to be(when_disabled) }
+ end
+ end
+ end
+
+ context 'with a project label' do
+ let(:label) { create(:label, project: project, title: 'bug') }
+
+ context 'when asking for an issue link' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues
+ end
+
+ context 'when asking for a merge requests link' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests
+ end
+ end
+
+ context 'with a group label' do
+ set(:group) { create(:group) }
+ let(:label) { create(:group_label, group: group, title: 'bug') }
+
+ context 'when asking for an issue link' do
+ context 'in the context of a project' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true
+ end
+
+ context 'in the context of a group' do
+ let(:context_project) { nil }
+
+ it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true
+ end
+ end
+
+ context 'when asking for a merge requests link' do
+ context 'in the context of a project' do
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true
+ end
+
+ context 'in the context of a group' do
+ let(:context_project) { nil }
+
+ it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true
+ end
+ end
+ end
+ end
+
describe 'link_to_label' do
let(:project) { create(:project) }
let(:label) { create(:label, project: project) }
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 62ea6d48542..ba0039f3a11 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -205,7 +205,7 @@ describe MarkupHelper do
it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page")
+ expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", issuable_state_filter_enabled: true)
helper.render_wiki_content(@wiki)
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 33186cf50d5..45ffbeb27a4 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -1,14 +1,6 @@
require 'spec_helper'
describe MembersHelper do
- describe '#action_member_permission' do
- let(:project_member) { build(:project_member) }
- let(:group_member) { build(:group_member) }
-
- it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member }
- it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
- end
-
describe '#remove_member_message' do
let(:requester) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) }
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index fd7900c32f4..3008528e60c 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe MergeRequestsHelper do
+ include ActionView::Helpers::UrlHelper
include ProjectForksHelper
+
describe 'ci_build_details_path' do
let(:project) { create(:project) }
let(:merge_request) { MergeRequest.new }
@@ -41,4 +43,19 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
+
+ describe '#tab_link_for' do
+ let(:merge_request) { create(:merge_request, :simple) }
+ let(:options) { Hash.new }
+
+ subject { tab_link_for(merge_request, :show, options) { 'Discussion' } }
+
+ describe 'supports the :force_link option' do
+ let(:options) { { force_link: true } }
+
+ it 'removes the data-toggle attributes' do
+ is_expected.not_to match(/data-toggle="tab"/)
+ end
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 8b8080563d3..749aa25e632 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -77,15 +77,6 @@ describe PreferencesHelper do
end
end
- def stub_user(messages = {})
- if messages.empty?
- allow(helper).to receive(:current_user).and_return(nil)
- else
- allow(helper).to receive(:current_user)
- .and_return(double('user', messages))
- end
- end
-
describe '#default_project_view' do
context 'user not signed in' do
before do
@@ -125,5 +116,70 @@ describe PreferencesHelper do
end
end
end
+
+ context 'user signed in' do
+ let(:user) { create(:user, :readme) }
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when the user is allowed to see the code' do
+ it 'returns the project view' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('readme')
+ end
+ end
+
+ context 'with wikis enabled and the right policy for the user' do
+ before do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ end
+
+ it 'returns wiki if the user has the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true)
+
+ expect(helper.default_project_view).to eq('wiki')
+ end
+
+ it 'returns customize_workflow if the user does not have the right policy' do
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+
+ context 'with issues as a feature available' do
+ it 'return issues' do
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('projects/issues/issues')
+ end
+ end
+
+ context 'with no activity, no wikies and no issues' do
+ it 'returns customize_workflow as default' do
+ project.project_feature.update_attribute(:issues_access_level, 0)
+ allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false)
+
+ expect(helper.default_project_view).to eq('customize_workflow')
+ end
+ end
+ end
+ end
+
+ def stub_user(messages = {})
+ if messages.empty?
+ allow(helper).to receive(:current_user).and_return(nil)
+ else
+ allow(helper).to receive(:current_user)
+ .and_return(double('user', messages))
+ end
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c358ccae9c3..d3b1be599dd 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -9,6 +9,7 @@ describe TreeHelper do
before do
@id = sha
@project = project
+ @lfs_blob_ids = []
end
it 'displays all entries without a warning' do
diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js
index e8c5f721423..fc9be14df8f 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/javascripts/activities_spec.js
@@ -2,7 +2,7 @@
import 'vendor/jquery.endless-scroll';
import '~/pager';
-import '~/activities';
+import Activities from '~/activities';
(() => {
window.gon || (window.gon = {});
@@ -35,7 +35,7 @@ import '~/activities';
describe('Activities', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- new gl.Activities();
+ new Activities();
});
for (let i = 0; i < filters.length; i += 1) {
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index f9fa814b801..8287c58ac5a 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,39 +1,43 @@
-/* eslint-disable space-before-function-paren, no-var */
-
import '~/behaviors/requires_input';
-(function() {
- describe('requiresInput', function() {
- preloadFixtures('branches/new_branch.html.raw');
- beforeEach(function() {
- loadFixtures('branches/new_branch.html.raw');
- this.submitButton = $('button[type="submit"]');
- });
- it('disables submit when any field is required', function() {
- $('.js-requires-input').requiresInput();
- return expect(this.submitButton).toBeDisabled();
- });
- it('enables submit when no field is required', function() {
- $('*[required=required]').removeAttr('required');
- $('.js-requires-input').requiresInput();
- return expect(this.submitButton).not.toBeDisabled();
- });
- it('enables submit when all required fields are pre-filled', function() {
- $('*[required=required]').remove();
- $('.js-requires-input').requiresInput();
- return expect($('.submit')).not.toBeDisabled();
- });
- it('enables submit when all required fields receive input', function() {
- $('.js-requires-input').requiresInput();
- $('#required1').val('input1').change();
- expect(this.submitButton).toBeDisabled();
- $('#optional1').val('input1').change();
- expect(this.submitButton).toBeDisabled();
- $('#required2').val('input2').change();
- $('#required3').val('input3').change();
- $('#required4').val('input4').change();
- $('#required5').val('1').change();
- return expect($('.submit')).not.toBeDisabled();
- });
+describe('requiresInput', () => {
+ let submitButton;
+ preloadFixtures('branches/new_branch.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('branches/new_branch.html.raw');
+ submitButton = $('button[type="submit"]');
+ });
+
+ it('disables submit when any field is required', () => {
+ $('.js-requires-input').requiresInput();
+ expect(submitButton).toBeDisabled();
+ });
+
+ it('enables submit when no field is required', () => {
+ $('*[required=required]').removeAttr('required');
+ $('.js-requires-input').requiresInput();
+ expect(submitButton).not.toBeDisabled();
+ });
+
+ it('enables submit when all required fields are pre-filled', () => {
+ $('*[required=required]').remove();
+ $('.js-requires-input').requiresInput();
+ expect($('.submit')).not.toBeDisabled();
+ });
+
+ it('enables submit when all required fields receive input', () => {
+ $('.js-requires-input').requiresInput();
+ $('#required1').val('input1').change();
+ expect(submitButton).toBeDisabled();
+
+ $('#optional1').val('input1').change();
+ expect(submitButton).toBeDisabled();
+
+ $('#required2').val('input2').change();
+ $('#required3').val('input3').change();
+ $('#required4').val('input4').change();
+ $('#required5').val('1').change();
+ expect($('.submit')).not.toBeDisabled();
});
-}).call(window);
+});
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 9e5b0bd3efe..0e656858182 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -9,7 +9,6 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index ccde657789a..41dcb19df3c 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -4,7 +4,6 @@
/* global mockBoardService */
import Vue from 'vue';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
@@ -146,6 +145,12 @@ describe('Issue model', () => {
expect(issue.isFetching.subscriptions).toBe(false);
});
+ it('sets loading state', () => {
+ issue.setLoadingState('foo', true);
+
+ expect(issue.isLoading.foo).toBe(true);
+ });
+
describe('update', () => {
it('passes assignee ids when there are assignees', (done) => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index d4627223a12..eead396ca7e 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -9,7 +9,6 @@
import Vue from 'vue';
-import '~/lib/utils/url_utility';
import '~/boards/models/issue';
import '~/boards/models/label';
import '~/boards/models/list';
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 027e8001053..f5be9ea0fb2 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -28,7 +28,7 @@ describe('Clusters', () => {
expect(
cluster.toggleButton.classList,
- ).not.toContain('checked');
+ ).not.toContain('is-checked');
expect(
cluster.toggleInput.getAttribute('value'),
@@ -36,6 +36,20 @@ describe('Clusters', () => {
});
});
+ describe('showToken', () => {
+ it('should update tye field type', () => {
+ cluster.showTokenButton.click();
+ expect(
+ cluster.tokenField.getAttribute('type'),
+ ).toEqual('text');
+
+ cluster.showTokenButton.click();
+ expect(
+ cluster.tokenField.getAttribute('type'),
+ ).toEqual('password');
+ });
+ });
+
describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' },
@@ -113,7 +127,7 @@ describe('Clusters', () => {
});
describe('when cluster is created', () => {
- it('should show the success container', () => {
+ it('should show the success container and fresh the page', () => {
cluster.updateContainer(null, 'created');
expect(
diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js
new file mode 100644
index 00000000000..0a8b63ed5b4
--- /dev/null
+++ b/spec/javascripts/clusters/clusters_index_spec.js
@@ -0,0 +1,58 @@
+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/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index 0f7bf9ec712..2e5b65f5610 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -1,110 +1,108 @@
import * as datetimeUtility from '~/lib/utils/datetime_utility';
-(() => {
- describe('Date time utils', () => {
- describe('timeFor', () => {
- it('returns `past due` when in past', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() - 1);
-
- expect(
- gl.utils.timeFor(date),
- ).toBe('Past due');
- });
-
- it('returns remaining time when in the future', () => {
- const date = new Date();
- date.setFullYear(date.getFullYear() + 1);
-
- // Add a day to prevent a transient error. If date is even 1 second
- // short of a full year, timeFor will return '11 months remaining'
- date.setDate(date.getDate() + 1);
-
- expect(
- gl.utils.timeFor(date),
- ).toBe('1 year remaining');
- });
+describe('Date time utils', () => {
+ describe('timeFor', () => {
+ it('returns `past due` when in past', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() - 1);
+
+ expect(
+ datetimeUtility.timeFor(date),
+ ).toBe('Past due');
});
- describe('get day name', () => {
- it('should return Sunday', () => {
- const day = gl.utils.getDayName(new Date('07/17/2016'));
- expect(day).toBe('Sunday');
- });
-
- it('should return Monday', () => {
- const day = gl.utils.getDayName(new Date('07/18/2016'));
- expect(day).toBe('Monday');
- });
-
- it('should return Tuesday', () => {
- const day = gl.utils.getDayName(new Date('07/19/2016'));
- expect(day).toBe('Tuesday');
- });
-
- it('should return Wednesday', () => {
- const day = gl.utils.getDayName(new Date('07/20/2016'));
- expect(day).toBe('Wednesday');
- });
-
- it('should return Thursday', () => {
- const day = gl.utils.getDayName(new Date('07/21/2016'));
- expect(day).toBe('Thursday');
- });
-
- it('should return Friday', () => {
- const day = gl.utils.getDayName(new Date('07/22/2016'));
- expect(day).toBe('Friday');
- });
-
- it('should return Saturday', () => {
- const day = gl.utils.getDayName(new Date('07/23/2016'));
- expect(day).toBe('Saturday');
- });
- });
+ it('returns remaining time when in the future', () => {
+ const date = new Date();
+ date.setFullYear(date.getFullYear() + 1);
+
+ // Add a day to prevent a transient error. If date is even 1 second
+ // short of a full year, timeFor will return '11 months remaining'
+ date.setDate(date.getDate() + 1);
- describe('get day difference', () => {
- it('should return 7', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('07/08/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(7);
- });
-
- it('should return 31', () => {
- const firstDay = new Date('07/01/2016');
- const secondDay = new Date('08/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(31);
- });
-
- it('should return 365', () => {
- const firstDay = new Date('07/02/2015');
- const secondDay = new Date('07/01/2016');
- const difference = gl.utils.getDayDifference(firstDay, secondDay);
- expect(difference).toBe(365);
- });
+ expect(
+ datetimeUtility.timeFor(date),
+ ).toBe('1 year remaining');
});
});
- describe('timeIntervalInWords', () => {
- it('should return string with number of minutes and seconds', () => {
- expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
- expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
- expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
- expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = datetimeUtility.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
});
});
- describe('dateInWords', () => {
- const date = new Date('07/01/2016');
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
- it('should return date in words', () => {
- expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
});
- it('should return abbreviated month name', () => {
- expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = datetimeUtility.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
});
});
-})();
+});
+
+describe('timeIntervalInWords', () => {
+ it('should return string with number of minutes and seconds', () => {
+ expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds');
+ expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second');
+ expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds');
+ expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds');
+ });
+});
+
+describe('dateInWords', () => {
+ const date = new Date('07/01/2016');
+
+ it('should return date in words', () => {
+ expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016');
+ });
+
+ it('should return abbreviated month name', () => {
+ expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016');
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
index 5b93fbc5575..7025c3d836c 100644
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -34,7 +34,7 @@ describe('Deploy keys action btn', () => {
setTimeout(() => {
expect(
eventHub.$emit,
- ).toHaveBeenCalledWith('enable.key', deployKey);
+ ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
done();
});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
index 700897f50b0..0ca9290d3d2 100644
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -139,4 +139,18 @@ describe('Deploy keys app component', () => {
it('hasKeys returns true when there are keys', () => {
expect(vm.hasKeys).toEqual(3);
});
+
+ it('resets remove button loading state', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+
+ const btn = vm.$el.querySelector('.btn-warning');
+
+ btn.click();
+
+ Vue.nextTick(() => {
+ expect(btn.querySelector('.fa')).toBeNull();
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index 5b64cbb2dfc..2f28c5bbf01 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import DeployKeysStore from '~/deploy_keys/store';
import key from '~/deploy_keys/components/key.vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
describe('Deploy keys key', () => {
let vm;
@@ -37,7 +38,7 @@ describe('Deploy keys key', () => {
it('renders human friendly formatted created date', () => {
expect(
vm.$el.querySelector('.key-created-at').textContent.trim(),
- ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`);
+ ).toBe(`created ${getTimeago().format(deployKey.created_at)}`);
});
it('shows edit button', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 230c15e5de6..5111632d681 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -1,8 +1,8 @@
+import * as urlUtils from '~/lib/utils/url_utility';
import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-import '~/lib/utils/url_utility';
import '~/lib/utils/common_utils';
import '~/filtered_search/filtered_search_token_keys';
import '~/filtered_search/filtered_search_tokenizer';
@@ -162,7 +162,7 @@ describe('Filtered Search Manager', () => {
it('should search with a single word', (done) => {
input.value = 'searchTerm';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
done();
});
@@ -173,7 +173,7 @@ describe('Filtered Search Manager', () => {
it('should search with multiple words', (done) => {
input.value = 'awesome search terms';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
done();
});
@@ -184,7 +184,7 @@ describe('Filtered Search Manager', () => {
it('should search with special characters', (done) => {
input.value = '~!@#$%^&*()_+{}:<>,.?/';
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
done();
});
@@ -198,7 +198,7 @@ describe('Filtered Search Manager', () => {
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
`);
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
done();
});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 8e74c4f859c..d26ea3febe8 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -31,4 +31,19 @@ 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/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 4f20e31f511..a3fa07d5bc2 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -253,7 +253,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -343,7 +343,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-icons-only');
+ sidebar.classList.add('sidebar-collapsed-desktop');
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index ca048123bf7..b13d1bf8dff 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -2,7 +2,7 @@
import '~/gl_dropdown';
import '~/lib/utils/common_utils';
-import '~/lib/utils/url_utility';
+import * as urlUtils from '~/lib/utils/url_utility';
describe('glDropdown', function describeDropdown() {
preloadFixtures('static/gl_dropdown.html.raw');
@@ -137,13 +137,13 @@ describe('glDropdown', function describeDropdown() {
expect(this.dropdownContainerElement).toHaveClass('open');
const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0;
navigateWithKeys('down', randomIndex, () => {
- spyOn(gl.utils, 'visitUrl').and.stub();
+ spyOn(urlUtils, 'visitUrl').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
const linkedLocation = link.attr('href');
- if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation);
+ if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation);
});
});
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 59d4f7c45c6..97e39f6411b 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
+import * as utils from '~/lib/utils/url_utility';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
-
import eventHub from '~/groups/event_hub';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
@@ -176,7 +176,7 @@ describe('AppComponent', () => {
it('should fetch groups for provided page details and update window state', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups));
spyOn(vm, 'updateGroups').and.callThrough();
- spyOn(gl.utils, 'mergeUrlParams').and.callThrough();
+ spyOn(utils, 'mergeUrlParams').and.callThrough();
spyOn(window.history, 'replaceState');
spyOn($, 'scrollTo');
@@ -192,7 +192,7 @@ describe('AppComponent', () => {
setTimeout(() => {
expect(vm.isLoading).toBeFalsy();
expect($.scrollTo).toHaveBeenCalledWith(0);
- expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
+ expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
page: jasmine.any(String),
}, jasmine.any(String), jasmine.any(String));
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 0f4fbdae445..618d0022e4f 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-
+import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
@@ -136,13 +136,13 @@ describe('GroupItemComponent', () => {
const group = Object.assign({}, mockParentGroupItem);
group.childrenCount = 0;
const newVm = createComponent(group);
- spyOn(gl.utils, 'visitUrl').and.stub();
+ spyOn(urlUtils, 'visitUrl').and.stub();
spyOn(eventHub, '$emit');
newVm.onClickRowGroup(event);
setTimeout(() => {
expect(eventHub.$emit).not.toHaveBeenCalled();
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath);
done();
}, 0);
});
diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js
index 2ce1a749a96..7a5c1da4d1d 100644
--- a/spec/javascripts/groups/components/item_actions_spec.js
+++ b/spec/javascripts/groups/components/item_actions_spec.js
@@ -36,27 +36,27 @@ describe('ItemActionsComponent', () => {
describe('methods', () => {
describe('onLeaveGroup', () => {
- it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => {
- expect(vm.dialogStatus).toBeFalsy();
+ it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => {
+ expect(vm.modalStatus).toBeFalsy();
vm.onLeaveGroup();
- expect(vm.dialogStatus).toBeTruthy();
+ expect(vm.modalStatus).toBeTruthy();
});
});
describe('leaveGroup', () => {
- it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
+ it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => {
spyOn(eventHub, '$emit');
- vm.dialogStatus = true;
+ vm.modalStatus = true;
vm.leaveGroup(true);
- expect(vm.dialogStatus).toBeFalsy();
+ expect(vm.modalStatus).toBeFalsy();
expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup);
});
- it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => {
+ it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => {
spyOn(eventHub, '$emit');
- vm.dialogStatus = true;
+ vm.modalStatus = true;
vm.leaveGroup(false);
- expect(vm.dialogStatus).toBeFalsy();
+ expect(vm.modalStatus).toBeFalsy();
expect(eventHub.$emit).not.toHaveBeenCalled();
});
});
@@ -99,9 +99,9 @@ describe('ItemActionsComponent', () => {
newVm.$destroy();
});
- it('should show modal dialog when `dialogStatus` is set to `true`', () => {
- vm.dialogStatus = true;
- const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog');
+ it('should show modal dialog when `modalStatus` is set to `true`', () => {
+ vm.modalStatus = true;
+ const modalDialogEl = vm.$el.querySelector('.modal');
expect(modalDialogEl).toBeDefined();
expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
index fb9c7e59031..ce3add1fd90 100644
--- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js
+++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js
@@ -89,15 +89,8 @@ describe('badge helper', () => {
});
it('should create icon comment button', () => {
- const iconEl = buttonEl.querySelector('i');
+ const iconEl = buttonEl.querySelector('svg');
expect(iconEl).toBeDefined();
- expect(iconEl.classList.contains('fa')).toEqual(true);
- expect(iconEl.classList.contains('fa-comment-o')).toEqual(true);
- });
-
- it('should have .image-comment-badge.inverted in button class', () => {
- expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true);
- expect(buttonEl.classList.contains('inverted')).toEqual(true);
});
});
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
index 56d77a05c4c..31949c39d9c 100644
--- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js
+++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
@@ -157,27 +157,19 @@ describe('utilsHelper', () => {
beforeEach(() => {
window.gl = window.gl || (window.gl = {});
glCache = window.gl;
- window.gl.ImageFile = () => {};
fileEl = document.createElement('div');
fileEl.innerHTML = `
<div class="diff-file"></div>
`;
- spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
});
afterEach(() => {
window.gl = glCache;
});
- it('should initialize gl.ImageFile', () => {
- spyOn(window.gl, 'ImageFile');
-
- utilsHelper.initImageDiff(fileEl, false, false);
- expect(gl.ImageFile).toHaveBeenCalled();
- });
-
it('should initialize ImageDiff if js-single-image', () => {
const diffFileEl = fileEl.querySelector('.diff-file');
diffFileEl.innerHTML = `
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index ceee08d47c5..5a9112716f4 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -26,7 +26,7 @@ describe('Issuable', () => {
document.body.appendChild(element);
const input = document.createElement('input');
- input.setAttribute('id', 'issue_email');
+ input.setAttribute('id', 'issuable_email');
document.body.appendChild(input);
Issuable = new IssuableIndex('issue_');
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index b47a8bf705f..7159148f8fa 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -1,9 +1,11 @@
import Vue from 'vue';
import '~/render_math';
import '~/render_gfm';
+import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -55,6 +57,8 @@ describe('Issuable output', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm.poll.stop();
+
+ vm.$destroy();
});
it('should render a title/description/edited and update title/description/edited on update', (done) => {
@@ -177,7 +181,7 @@ describe('Issuable output', () => {
});
it('does not redirect if issue has not moved', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
@@ -193,7 +197,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).not.toHaveBeenCalled();
done();
@@ -201,7 +205,7 @@ describe('Issuable output', () => {
});
it('redirects if returned web_url has changed', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
@@ -217,7 +221,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).toHaveBeenCalledWith('/testing-issue-move');
done();
@@ -268,9 +272,55 @@ describe('Issuable output', () => {
});
});
+ it('opens recaptcha modal if update rejected as spam', (done) => {
+ function mockScriptSrc() {
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+ }
+
+ let modal;
+ const promise = new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ };
+ },
+ });
+ });
+
+ spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
+
+ vm.canUpdate = true;
+ vm.showForm = true;
+
+ vm.$nextTick()
+ .then(() => mockScriptSrc())
+ .then(() => vm.updateIssuable())
+ .then(promise)
+ .then(() => setTimeoutPromise())
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
json() {
@@ -283,7 +333,7 @@ describe('Issuable output', () => {
setTimeout(() => {
expect(
- gl.utils.visitUrl,
+ urlUtils.visitUrl,
).toHaveBeenCalledWith('/test');
done();
@@ -291,7 +341,7 @@ describe('Issuable output', () => {
});
it('stops polling when deleting', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn(vm.poll, 'stop').and.callThrough();
spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => {
resolve({
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 163e5cdd062..0da25bdca9c 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -51,6 +51,35 @@ describe('Description component', () => {
});
});
+ it('opens recaptcha dialog if update rejected as spam', (done) => {
+ let modal;
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+
+ vm.taskListUpdateSuccess({
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-modal');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('TaskList', () => {
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
@@ -86,6 +115,7 @@ describe('Description component', () => {
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
+ onSuccess: jasmine.any(Function),
});
done();
});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index 6e89528a3ea..000b53af016 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -34,7 +34,6 @@ describe('Inline edit form component', () => {
});
it('renders template selector when templates exists', (done) => {
- spyOn(gl, 'IssuableTemplateSelectors');
vm.issuableTemplates = ['test'];
Vue.nextTick(() => {
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 3636aac79a0..2cd2e63b15d 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -55,7 +55,7 @@ describe('Issue', function() {
}
function findElements(isIssueInitiallyOpen) {
- $boxClosed = $('div.status-box-closed');
+ $boxClosed = $('div.status-box-issue-closed');
expect($boxClosed).toExist();
expect($boxClosed).toHaveText('Closed');
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index 5e67911d338..4f06237deb5 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,6 +1,6 @@
import { bytesToKiB } from '~/lib/utils/number_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
-import '~/lib/utils/url_utility';
import Job from '~/job';
import '~/breakpoints';
@@ -28,7 +28,7 @@ describe('Job', () => {
});
it('copies build options', function () {
- expect(this.job.pageUrl).toBe(JOB_URL);
+ expect(this.job.pagePath).toBe(JOB_URL);
expect(this.job.buildStatus).toBe('success');
expect(this.job.buildStage).toBe('test');
expect(this.job.state).toBe('');
@@ -65,7 +65,7 @@ describe('Job', () => {
const deferred2 = $.Deferred();
const deferred3 = $.Deferred();
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>',
@@ -103,7 +103,7 @@ describe('Job', () => {
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update<span>',
@@ -134,7 +134,7 @@ describe('Job', () => {
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
@@ -153,7 +153,7 @@ describe('Job', () => {
it('shows the size in KiB', () => {
const size = 50;
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
@@ -179,7 +179,7 @@ describe('Job', () => {
spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
deferred1.resolve({
html: '<span>Update</span>',
@@ -214,7 +214,7 @@ describe('Job', () => {
it('renders the raw link', () => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
@@ -236,7 +236,7 @@ describe('Job', () => {
describe('when size is equal than total', () => {
it('does not show the trunctated information', () => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
@@ -257,7 +257,7 @@ describe('Job', () => {
describe('output trace', () => {
beforeEach(() => {
const deferred = $.Deferred();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
spyOn($, 'ajax').and.returnValue(deferred.promise());
deferred.resolve({
diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
index e58ac4300ba..a9f3abcf2a4 100644
--- a/spec/javascripts/lib/utils/datefix_spec.js
+++ b/spec/javascripts/lib/utils/datefix_spec.js
@@ -21,7 +21,7 @@ describe('datefix', () => {
describe('pikadayToString', () => {
it('should format a UTC date into yyyy-mm-dd format', () => {
- expect(pikadayToString(new Date('2020-01-29'))).toEqual('2020-01-29');
+ expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
});
});
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 3ab901da6b6..70ae63ba036 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -63,7 +63,7 @@ import IssuablesHelper from '~/helpers/issuables_helper';
describe('merge request of another user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- this.el = document.querySelector('.merge-request .issuable-actions');
+ this.el = document.querySelector('.js-issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
@@ -83,7 +83,7 @@ import IssuablesHelper from '~/helpers/issuables_helper';
describe('merge request of current_user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
- this.el = document.querySelector('.merge-request .issuable-actions');
+ this.el = document.querySelector('.js-issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index e441d1153ed..5076435e7a8 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
/* global Notes */
+import * as urlUtils from '~/lib/utils/url_utility';
import '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/breakpoints';
@@ -333,7 +334,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteId.length).toBeGreaterThan(0);
@@ -345,7 +346,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
@@ -354,7 +355,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
@@ -387,7 +388,7 @@ import 'vendor/jquery.scrollTo';
describe('with note fragment hash', () => {
it('should expand and scroll to linked fragment hash #note_xxx', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
@@ -400,7 +401,7 @@ import 'vendor/jquery.scrollTo';
});
it('should gracefully ignore non-existant fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(window.notes.toggleDiffNote).not.toHaveBeenCalled();
@@ -409,7 +410,7 @@ import 'vendor/jquery.scrollTo';
describe('with line number fragment hash', () => {
it('should gracefully ignore line number fragment hash', function () {
- spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
expect(noteLineNumId.length).toBeGreaterThan(0);
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index dea42d755d4..bf6ada8185e 100644
--- a/spec/javascripts/monitoring/graph/deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -118,7 +118,7 @@ describe('MonitoringDeployment', () => {
).not.toEqual('display: none;');
});
- it('shows the refText inside a text element with the deploy-info-text class', () => {
+ it('contains date, refs and the "deployed" text', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
@@ -129,8 +129,31 @@ describe('MonitoringDeployment', () => {
});
expect(
- component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
- ).toEqual(component.refText(reducedDeploymentData[0]));
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText('Deployed');
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText('Wed, May 31');
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText(component.refText(reducedDeploymentData[0]));
+ });
+
+ it('contains a link to the commit contents', () => {
+ reducedDeploymentData[0].showDeploymentFlag = true;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphWidth: 440,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'),
+ ).not.toEqual('');
});
it('should contain a hidden gradient', () => {
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index fd79abe241a..b1d69752bad 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -4,6 +4,8 @@ import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub';
import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
+const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
+const projectPath = 'http://test.host/frontend-fixtures/environments-project';
const createComponent = (propsData) => {
const Component = Vue.extend(Graph);
@@ -25,6 +27,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
@@ -37,6 +41,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const transformedHeight = `${component.graphHeight - 100}`;
@@ -50,6 +56,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const viewBoxArray = component.outerViewBox.split(' ');
@@ -65,6 +73,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
spyOn(eventHub, '$emit');
@@ -81,6 +91,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.yAxisLabel).toEqual(component.graphData.y_label);
@@ -98,6 +110,8 @@ describe('Graph', () => {
hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
currentDeployXPos: null,
},
+ tagsPath,
+ projectPath,
});
component.positionFlag();
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6b34855b8b2..1f4e858e731 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -2430,33 +2430,39 @@ export const deploymentData = [
id: 111,
iid: 3,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-31T21:23:37.881Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': true
},
{
id: 110,
iid: 2,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-30T20:08:04.629Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
},
{
id: 109,
iid: 1,
sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
ref: {
name: 'update2-readme'
},
created_at: '2017-05-30T17:42:38.409Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
}
];
diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index db75262b562..20e352dd8bd 100644
--- a/spec/javascripts/notes/components/issue_comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Autosize from 'autosize';
import store from '~/notes/stores';
-import issueCommentForm from '~/notes/components/issue_comment_form.vue';
-import { loggedOutIssueData, notesDataMock, userDataMock, issueDataMock } from '../mock_data';
+import issueCommentForm from '~/notes/components/comment_form.vue';
+import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => {
@@ -23,7 +23,7 @@ describe('issue_comment_form component', () => {
describe('user is logged in', () => {
beforeEach(() => {
store.dispatch('setUserData', userDataMock);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = mountComponent();
@@ -178,7 +178,7 @@ describe('issue_comment_form component', () => {
describe('issue is confidential', () => {
it('shows information warning', (done) => {
- store.dispatch('setIssueData', Object.assign(issueDataMock, { confidential: true }));
+ store.dispatch('setNoteableData', Object.assign(noteableDataMock, { confidential: true }));
Vue.nextTick(() => {
expect(vm.$el.querySelector('.confidential-issue-warning')).toBeDefined();
done();
@@ -190,7 +190,7 @@ describe('issue_comment_form component', () => {
describe('user is not logged in', () => {
beforeEach(() => {
store.dispatch('setUserData', null);
- store.dispatch('setIssueData', loggedOutIssueData);
+ store.dispatch('setNoteableData', loggedOutnoteableData);
store.dispatch('setNotesData', notesDataMock);
vm = mountComponent();
diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index 7bcc061f167..ab81aabb992 100644
--- a/spec/javascripts/notes/components/issue_note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueActions from '~/notes/components/issue_note_actions.vue';
+import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
describe('issse_note_actions component', () => {
@@ -8,7 +8,7 @@ describe('issse_note_actions component', () => {
let Component;
beforeEach(() => {
- Component = Vue.extend(issueActions);
+ Component = Vue.extend(noteActions);
});
afterEach(() => {
diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 22e91c4c40f..7c8d6685ee1 100644
--- a/spec/javascripts/notes/components/issue_note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -1,30 +1,19 @@
import Vue from 'vue';
-import issueNotesApp from '~/notes/components/issue_notes_app.vue';
-import service from '~/notes/services/issue_notes_service';
+import notesApp from '~/notes/components/notes_app.vue';
+import service from '~/notes/services/notes_service';
import * as mockData from '../mock_data';
+import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-describe('issue_note_app', () => {
+describe('note_app', () => {
let mountComponent;
let vm;
- const individualNoteInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), {
- status: 200,
- }));
- };
-
- const discussionNoteInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), {
- status: 200,
- }));
- };
-
beforeEach(() => {
- const IssueNotesApp = Vue.extend(issueNotesApp);
+ const IssueNotesApp = Vue.extend(notesApp);
mountComponent = (data) => {
const props = data || {
- issueData: mockData.issueDataMock,
+ noteableData: mockData.noteableDataMock,
notesData: mockData.notesDataMock,
userData: mockData.userDataMock,
};
@@ -60,7 +49,7 @@ describe('issue_note_app', () => {
});
it('should set issue data', () => {
- expect(vm.$store.state.issueData).toEqual(mockData.issueDataMock);
+ expect(vm.$store.state.noteableData).toEqual(mockData.noteableDataMock);
});
it('should set user data', () => {
@@ -74,16 +63,16 @@ describe('issue_note_app', () => {
describe('render', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
it('should render list of notes', (done) => {
- const note = mockData.individualNoteServerResponse[0].notes[0];
+ const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0];
setTimeout(() => {
expect(
@@ -129,13 +118,16 @@ describe('issue_note_app', () => {
describe('update note', () => {
describe('individual note', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
- spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
+ spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ mockData.individualNoteInterceptor,
+ );
});
it('renders edit form', (done) => {
@@ -149,28 +141,36 @@ describe('issue_note_app', () => {
});
it('calls the service to update the note', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-note-edit').click();
- Vue.nextTick(() => {
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
- done();
- });
- }, 0);
+ })
+ // Wait for the requests to finish before destroying
+ .then(Vue.nextTick)
+ .then(done)
+ .catch(done.fail);
});
});
describe('dicussion note', () => {
beforeEach(() => {
- Vue.http.interceptors.push(discussionNoteInterceptor);
- spyOn(service, 'updateNote').and.callFake(() => Promise.resolve());
+ Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
+ spyOn(service, 'updateNote').and.callThrough();
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor);
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors,
+ mockData.discussionNoteInterceptor,
+ );
});
it('renders edit form', (done) => {
@@ -184,16 +184,21 @@ describe('issue_note_app', () => {
});
it('updates the note and resets the edit form', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-note-edit').click();
- Vue.nextTick(() => {
+ getSetTimeoutPromise()
+ .then(() => {
+ vm.$el.querySelector('.js-note-edit').click();
+ })
+ .then(Vue.nextTick)
+ .then(() => {
vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note';
vm.$el.querySelector('.js-vue-issue-save').click();
expect(service.updateNote).toHaveBeenCalled();
- done();
- });
- }, 0);
+ })
+ // Wait for the requests to finish before destroying
+ .then(Vue.nextTick)
+ .then(done)
+ .catch(done.fail);
});
});
});
@@ -216,12 +221,12 @@ describe('issue_note_app', () => {
describe('edit form', () => {
beforeEach(() => {
- Vue.http.interceptors.push(individualNoteInterceptor);
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
vm = mountComponent();
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor);
+ Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
});
it('should render markdown docs url', (done) => {
diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/note_attachment_spec.js
index 8f33b874ad6..b14a518b622 100644
--- a/spec/javascripts/notes/components/issue_note_attachment_spec.js
+++ b/spec/javascripts/notes/components/note_attachment_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue';
+import noteAttachment from '~/notes/components/note_attachment.vue';
describe('issue note attachment', () => {
it('should render properly', () => {
@@ -11,7 +11,7 @@ describe('issue note attachment', () => {
},
};
- const Component = Vue.extend(issueNoteAttachment);
+ const Component = Vue.extend(noteAttachment);
const vm = new Component({
propsData: props,
}).$mount();
diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js
index 3b6c34f1494..15995ec5a05 100644
--- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js
+++ b/spec/javascripts/notes/components/note_awards_list_spec.js
@@ -1,16 +1,16 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import awardsNote from '~/notes/components/issue_note_awards_list.vue';
-import { issueDataMock, notesDataMock } from '../mock_data';
+import awardsNote from '~/notes/components/note_awards_list.vue';
+import { noteableDataMock, notesDataMock } from '../mock_data';
-describe('issue_note_awards_list component', () => {
+describe('note_awards_list component', () => {
let vm;
let awardsMock;
beforeEach(() => {
const Component = Vue.extend(awardsNote);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
awardsMock = [
{
diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index 81f07ed47cc..b42e7943b98 100644
--- a/spec/javascripts/notes/components/issue_note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import noteBody from '~/notes/components/issue_note_body.vue';
-import { issueDataMock, notesDataMock, note } from '../mock_data';
+import noteBody from '~/notes/components/note_body.vue';
+import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note_body component', () => {
let vm;
@@ -10,7 +10,7 @@ describe('issue_note_body component', () => {
beforeEach(() => {
const Component = Vue.extend(noteBody);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js
index 6603241eb64..e0b991c32ec 100644
--- a/spec/javascripts/notes/components/issue_note_edited_text_spec.js
+++ b/spec/javascripts/notes/components/note_edited_text_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
-import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue';
+import noteEditedText from '~/notes/components/note_edited_text.vue';
-describe('issue_note_edited_text', () => {
+describe('note_edited_text', () => {
let vm;
let props;
beforeEach(() => {
- const Component = Vue.extend(issueNoteEditedText);
+ const Component = Vue.extend(noteEditedText);
props = {
actionText: 'Edited',
className: 'foo-bar',
diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index a90dbcb72b5..86e9e2a32a9 100644
--- a/spec/javascripts/notes/components/issue_note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueNoteForm from '~/notes/components/issue_note_form.vue';
-import { issueDataMock, notesDataMock } from '../mock_data';
+import issueNoteForm from '~/notes/components/note_form.vue';
+import { noteableDataMock, notesDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => {
@@ -11,7 +11,7 @@ describe('issue_note_form component', () => {
beforeEach(() => {
const Component = Vue.extend(issueNoteForm);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
props = {
diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 83ea18508ae..16a76b11321 100644
--- a/spec/javascripts/notes/components/issue_note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -1,13 +1,13 @@
import Vue from 'vue';
-import issueNoteHeader from '~/notes/components/issue_note_header.vue';
+import noteHeader from '~/notes/components/note_header.vue';
import store from '~/notes/stores';
-describe('issue_note_header component', () => {
+describe('note_header component', () => {
let vm;
let Component;
beforeEach(() => {
- Component = Vue.extend(issueNoteHeader);
+ Component = Vue.extend(noteHeader);
});
afterEach(() => {
diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
index f20d9ce9268..6cba8053888 100644
--- a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js
+++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js
@@ -1,13 +1,13 @@
import Vue from 'vue';
-import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue';
+import noteSignedOut from '~/notes/components/note_signed_out_widget.vue';
import store from '~/notes/stores';
import { notesDataMock } from '../mock_data';
-describe('issue_note_signed_out_widget component', () => {
+describe('note_signed_out_widget component', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(issueNoteSignedOut);
+ const Component = Vue.extend(noteSignedOut);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 05c6b57f93e..19504e4f7c8 100644
--- a/spec/javascripts/notes/components/issue_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueDiscussion from '~/notes/components/issue_discussion.vue';
-import { issueDataMock, discussionMock, notesDataMock } from '../mock_data';
+import issueDiscussion from '~/notes/components/noteable_discussion.vue';
+import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
describe('issue_discussion component', () => {
let vm;
@@ -9,7 +9,7 @@ describe('issue_discussion component', () => {
beforeEach(() => {
const Component = Vue.extend(issueDiscussion);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
@@ -30,7 +30,7 @@ describe('issue_discussion component', () => {
it('should render discussion header', () => {
expect(vm.$el.querySelector('.discussion-header')).toBeDefined();
- expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length);
+ expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
});
describe('actions', () => {
diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js
index 7ef85d5b4f0..c8a6cb7e612 100644
--- a/spec/javascripts/notes/components/issue_note_spec.js
+++ b/spec/javascripts/notes/components/noteable_note_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import store from '~/notes/stores';
-import issueNote from '~/notes/components/issue_note.vue';
-import { issueDataMock, notesDataMock, note } from '../mock_data';
+import issueNote from '~/notes/components/noteable_note.vue';
+import { noteableDataMock, notesDataMock, note } from '../mock_data';
describe('issue_note', () => {
let vm;
@@ -10,7 +10,7 @@ describe('issue_note', () => {
beforeEach(() => {
const Component = Vue.extend(issueNote);
- store.dispatch('setIssueData', issueDataMock);
+ store.dispatch('setNoteableData', noteableDataMock);
store.dispatch('setNotesData', notesDataMock);
vm = new Component({
@@ -41,4 +41,19 @@ describe('issue_note', () => {
it('should render issue body', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
+
+ it('prevents note preview xss', (done) => {
+ const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+ const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`;
+ const alertSpy = spyOn(window, 'alert');
+ vm.updateNote = () => new Promise($.noop);
+
+ vm.formUpdateHandler(noteBody, null, $.noop);
+
+ setTimeout(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.note.note_html).toEqual(_.escape(noteBody));
+ done();
+ }, 0);
+ });
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 89ba3a002b7..6b608adff15 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -18,7 +18,7 @@ export const userDataMock = {
username: 'root',
};
-export const issueDataMock = {
+export const noteableDataMock = {
assignees: [],
author_id: 1,
branch_name: null,
@@ -271,7 +271,7 @@ export const discussionMock = {
individual_note: false,
};
-export const loggedOutIssueData = {
+export const loggedOutnoteableData = {
"id": 98,
"iid": 26,
"author_id": 1,
@@ -312,138 +312,212 @@ export const loggedOutIssueData = {
"preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue"
}
-export const individualNoteServerResponse = [{
- "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "expanded": true,
- "notes": [{
- "id": 1390,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-01T17:09:33.762Z",
- "updated_at": "2017-08-01T17:09:33.762Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "sdfdsaf",
- "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
- "current_user": {
- "can_edit": true
+export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
+ 'GET': {
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
+ "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "expanded": true,
+ "notes": [{
+ "id": 1390,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-01T17:09:33.762Z",
+ "updated_at": "2017-08-01T17:09:33.762Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "sdfdsaf",
+ "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
+ "emoji_awardable": true,
+ "award_emoji": [{
+ "name": "baseball",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }, {
+ "name": "art",
+ "user": {
+ "id": 1,
+ "name": "Root",
+ "username": "root"
+ }
+ }],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1390"
+ }],
+ "individual_note": true
+ }, {
+ "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "expanded": true,
+ "notes": [{
+ "id": 1391,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-02T10:51:38.685Z",
+ "updated_at": "2017-08-02T10:51:38.685Z",
+ "system": false,
+ "noteable_id": 98,
+ "noteable_type": "Issue",
+ "type": null,
+ "human_access": "Owner",
+ "note": "New note!",
+ "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1391"
+ }],
+ "individual_note": true
+ }],
+ '/gitlab-org/gitlab-ce/noteable/issue/98/notes': {
+ last_fetched_at: 1512900838,
+ notes: [],
},
- "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
- "emoji_awardable": true,
- "award_emoji": [{
- "name": "baseball",
- "user": {
+ },
+ 'PUT': {
+ '/gitlab-org/gitlab-ce/notes/1471': {
+ "commands_changes": null,
+ "valid": true,
+ "id": 1471,
+ "attachment": null,
+ "author": {
"id": 1,
"name": "Root",
- "username": "root"
- }
- }, {
- "name": "art",
- "user": {
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-08T16:53:00.666Z",
+ "updated_at": "2017-12-10T11:03:21.876Z",
+ "system": false,
+ "noteable_id": 124,
+ "noteable_type": "Issue",
+ "noteable_iid": 29,
+ "type": "DiscussionNote",
+ "human_access": "Owner",
+ "note": "Adding a comment",
+ "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
+ "last_edited_at": "2017-12-10T11:03:21.876Z",
+ "last_edited_by": {
"id": 1,
- "name": "Root",
- "username": "root"
- }
- }],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1390"
- }],
- "individual_note": true
- }, {
- "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "expanded": true,
- "notes": [{
- "id": 1391,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-02T10:51:38.685Z",
- "updated_at": "2017-08-02T10:51:38.685Z",
- "system": false,
- "noteable_id": 98,
- "noteable_type": "Issue",
- "type": null,
- "human_access": "Owner",
- "note": "New note!",
- "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
- "current_user": {
- "can_edit": true
+ "name": 'Root',
+ "username": 'root',
+ "state": 'active',
+ "avatar_url": null,
+ "path": '/root',
+ },
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1471"
},
- "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1391"
- }],
- "individual_note": true
-}];
+ }
+};
-export const discussionNoteServerResponse = [{
- "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "expanded": true,
- "notes": [{
- "id": 1471,
- "attachment": {
- "url": null,
- "filename": null,
- "image": false
- },
- "author": {
- "id": 1,
- "name": "Root",
- "username": "root",
- "state": "active",
- "avatar_url": null,
- "path": "/root"
- },
- "created_at": "2017-08-08T16:53:00.666Z",
- "updated_at": "2017-08-08T16:53:00.666Z",
- "system": false,
- "noteable_id": 124,
- "noteable_type": "Issue",
- "noteable_iid": 29,
- "type": "DiscussionNote",
- "human_access": "Owner",
- "note": "Adding a comment",
- "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
- "current_user": {
- "can_edit": true
- },
- "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
- "emoji_awardable": true,
- "award_emoji": [],
- "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
- "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
- "path": "/gitlab-org/gitlab-ce/notes/1471"
- }],
- "individual_note": false
-}];
+export const DISCUSSION_NOTE_RESPONSE_MAP = {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP,
+ 'GET': {
+ ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
+ '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
+ "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "expanded": true,
+ "notes": [{
+ "id": 1471,
+ "attachment": {
+ "url": null,
+ "filename": null,
+ "image": false
+ },
+ "author": {
+ "id": 1,
+ "name": "Root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": null,
+ "path": "/root"
+ },
+ "created_at": "2017-08-08T16:53:00.666Z",
+ "updated_at": "2017-08-08T16:53:00.666Z",
+ "system": false,
+ "noteable_id": 124,
+ "noteable_type": "Issue",
+ "noteable_iid": 29,
+ "type": "DiscussionNote",
+ "human_access": "Owner",
+ "note": "Adding a comment",
+ "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
+ "current_user": {
+ "can_edit": true
+ },
+ "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
+ "emoji_awardable": true,
+ "award_emoji": [],
+ "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
+ "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
+ "path": "/gitlab-org/gitlab-ce/notes/1471"
+ }],
+ "individual_note": false
+ }],
+ },
+};
+
+export function individualNoteInterceptor(request, next) {
+ const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+}
+
+export function discussionNoteInterceptor(request, next) {
+ const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+}
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 3d1ca870ca4..e092320f9a3 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,6 +1,6 @@
import * as actions from '~/notes/stores/actions';
import testAction from '../../helpers/vuex_action_helper';
-import { discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Actions Notes Store', () => {
describe('setNotesData', () => {
@@ -11,10 +11,10 @@ describe('Actions Notes Store', () => {
});
});
- describe('setIssueData', () => {
+ describe('setNoteableData', () => {
it('should set received issue data', (done) => {
- testAction(actions.setIssueData, null, { issueData: {} }, [
- { type: 'SET_ISSUE_DATA', payload: issueDataMock },
+ testAction(actions.setNoteableData, null, { noteableData: {} }, [
+ { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock },
], done);
});
});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 48ee1bf9a52..c5a84b71788 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -1,5 +1,5 @@
import * as getters from '~/notes/stores/getters';
-import { notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Getters Notes Store', () => {
let state;
@@ -11,7 +11,7 @@ describe('Getters Notes Store', () => {
notesData: notesDataMock,
userData: userDataMock,
- issueData: issueDataMock,
+ noteableData: noteableDataMock,
};
});
describe('notes', () => {
@@ -32,9 +32,9 @@ describe('Getters Notes Store', () => {
});
});
- describe('getIssueData', () => {
- it('should return all data in `issueData`', () => {
- expect(getters.getIssueData(state)).toEqual(issueDataMock);
+ describe('getNoteableData', () => {
+ it('should return all data in `noteableData`', () => {
+ expect(getters.getNoteableData(state)).toEqual(noteableDataMock);
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 1e22e03e178..22d99998a7d 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,5 +1,5 @@
import mutations from '~/notes/stores/mutations';
-import { note, discussionMock, notesDataMock, userDataMock, issueDataMock, individualNote } from '../mock_data';
+import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
describe('Mutation Notes Store', () => {
describe('ADD_NEW_NOTE', () => {
@@ -74,14 +74,14 @@ describe('Mutation Notes Store', () => {
});
});
- describe('SET_ISSUE_DATA', () => {
+ describe('SET_NOTEABLE_DATA', () => {
it('should set the issue data', () => {
const state = {
- issueData: {},
+ noteableData: {},
};
- mutations.SET_ISSUE_DATA(state, issueDataMock);
- expect(state.issueData).toEqual(issueDataMock);
+ mutations.SET_NOTEABLE_DATA(state, noteableDataMock);
+ expect(state.noteableData).toEqual(noteableDataMock);
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 677a389b88f..e09b8dc7fc5 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
/* global Notes */
+import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
@@ -168,8 +169,7 @@ import '~/notes';
});
it('sets target when hash matches', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue(hash);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(hash);
Notes.updateNoteTargetSelector($note);
@@ -178,8 +178,7 @@ import '~/notes';
});
it('unsets target when hash does not match', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue('note_doesnotexist');
+ spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
@@ -187,8 +186,7 @@ import '~/notes';
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOn(gl.utils, 'getLocationHash');
- gl.utils.getLocationHash.and.returnValue(null);
+ spyOn(urlUtils, 'getLocationHash').and.returnValue(null);
Notes.updateNoteTargetSelector($note);
@@ -224,7 +222,6 @@ import '~/notes';
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
- spyOn(gl.utils, 'localTimeAgo');
spyOn(Notes, 'isNewNote').and.callThrough();
spyOn(Notes, 'isUpdatedNote').and.callThrough();
spyOn(Notes, 'animateAppendNote').and.callThrough();
@@ -351,7 +348,6 @@ import '~/notes';
]);
notes.note_ids = [];
- spyOn(gl.utils, 'localTimeAgo');
spyOn(Notes, 'isNewNote');
spyOn(Notes, 'animateAppendNote');
Notes.isNewNote.and.returnValue(true);
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index 1d3e1263371..fe3ea996eac 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,5 +1,6 @@
/* global fixture */
+import * as utils from '~/lib/utils/url_utility';
import '~/pager';
describe('pager', () => {
@@ -30,7 +31,7 @@ describe('pager', () => {
it('should use current url if data-href attribute not provided', () => {
const href = `${gl.TEST_HOST}/some_list`;
- spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ spyOn(utils, 'removeParams').and.returnValue(href);
Pager.init();
expect(Pager.url).toBe(href);
});
@@ -44,9 +45,9 @@ describe('pager', () => {
it('keeps extra query parameters from url', () => {
window.history.replaceState({}, null, '?filter=test&offset=100');
const href = `${gl.TEST_HOST}/some_list?filter=test`;
- spyOn(gl.utils, 'removeParams').and.returnValue(href);
+ spyOn(utils, 'removeParams').and.returnValue(href);
Pager.init();
- expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
+ expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']);
expect(Pager.url).toEqual(href);
});
});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 342ee6c1242..35e36e9c353 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -1,8 +1,10 @@
import Vue from 'vue';
import jobComponent from '~/pipelines/components/graph/job_component.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph job component', () => {
let JobComponent;
+ let component;
const mockJob = {
id: 4256,
@@ -13,6 +15,7 @@ describe('pipeline graph job component', () => {
label: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4256',
+ has_details: true,
action: {
icon: 'retry',
title: 'Retry',
@@ -26,13 +29,13 @@ describe('pipeline graph job component', () => {
JobComponent = Vue.extend(jobComponent);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
describe('name with link', () => {
it('should render the job name and status with a link', (done) => {
- const component = new JobComponent({
- propsData: {
- job: mockJob,
- },
- }).$mount();
+ component = mountComponent(JobComponent, { job: mockJob });
Vue.nextTick(() => {
const link = component.$el.querySelector('a');
@@ -56,23 +59,23 @@ describe('pipeline graph job component', () => {
describe('name without link', () => {
it('it should render status and name', () => {
- const component = new JobComponent({
- propsData: {
- job: {
- id: 4256,
- name: 'test',
- status: {
- icon: 'icon_status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- details_path: '/root/ci-mock/builds/4256',
- },
+ component = mountComponent(JobComponent, {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ details_path: '/root/ci-mock/builds/4256',
+ has_details: false,
},
},
- }).$mount();
+ });
expect(component.$el.querySelector('.js-status-icon-success')).toBeDefined();
+ expect(component.$el.querySelector('a')).toBeNull();
expect(
component.$el.querySelector('.ci-status-text').textContent.trim(),
@@ -82,11 +85,7 @@ describe('pipeline graph job component', () => {
describe('action icon', () => {
it('it should render the action icon', () => {
- const component = new JobComponent({
- propsData: {
- job: mockJob,
- },
- }).$mount();
+ component = mountComponent(JobComponent, { job: mockJob });
expect(component.$el.querySelector('a.ci-action-icon-container')).toBeDefined();
expect(component.$el.querySelector('i.ci-action-icon-wrapper')).toBeDefined();
@@ -95,27 +94,54 @@ describe('pipeline graph job component', () => {
describe('dropdown', () => {
it('should render the dropdown action icon', () => {
- const component = new JobComponent({
- propsData: {
- job: mockJob,
- isDropdown: true,
- },
- }).$mount();
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ isDropdown: true,
+ });
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
});
});
it('should render provided class name', () => {
- const component = new JobComponent({
- propsData: {
- job: mockJob,
- cssClassJobName: 'css-class-job-name',
- },
- }).$mount();
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ });
expect(
component.$el.querySelector('a').classList.contains('css-class-job-name'),
).toBe(true);
});
+
+ describe('status label', () => {
+ it('should not render status label when it is not provided', () => {
+ component = mountComponent(JobComponent, {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test');
+ });
+
+ it('should not render status label when it is provided', () => {
+ component = mountComponent(JobComponent, {
+ job: {
+ id: 4256,
+ name: 'test',
+ status: {
+ icon: 'icon_status_success',
+ label: 'success',
+ },
+ },
+ });
+
+ expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success');
+ });
+ });
});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index 1c794123095..72712e058e5 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/repo/stores';
import service from '~/repo/services';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
@@ -97,7 +98,7 @@ describe('RepoCommitSection', () => {
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
vm.startNewMR = true;
vm.makeCommit();
@@ -105,7 +106,7 @@ describe('RepoCommitSection', () => {
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalled();
+ expect(urlUtils.visitUrl).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 979d2185076..81158cad639 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,12 +1,13 @@
import Vue from 'vue';
import store from '~/repo/stores';
import repoEditor from '~/repo/components/repo_editor.vue';
+import monacoLoader from '~/repo/monaco_loader';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
let vm;
- beforeEach(() => {
+ beforeEach((done) => {
const f = file();
const RepoEditor = Vue.extend(repoEditor);
@@ -21,6 +22,10 @@ describe('RepoEditor', () => {
vm.monaco = true;
vm.$mount();
+
+ monacoLoader(['vs/editor/editor.main'], () => {
+ setTimeout(done, 0);
+ });
});
afterEach(() => {
@@ -32,7 +37,6 @@ describe('RepoEditor', () => {
it('renders an ide container', (done) => {
Vue.nextTick(() => {
expect(vm.shouldHideEditor).toBeFalsy();
- expect(vm.$el.textContent.trim()).toBe('');
done();
});
@@ -50,7 +54,7 @@ describe('RepoEditor', () => {
});
it('shows activeFile html', () => {
- expect(vm.$el.textContent.trim()).toBe('testing');
+ expect(vm.$el.textContent).toContain('testing');
});
});
});
diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js
new file mode 100644
index 00000000000..62c3913bf4d
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/disposable_spec.js
@@ -0,0 +1,44 @@
+import Disposable from '~/repo/lib/common/disposable';
+
+describe('Multi-file editor library disposable class', () => {
+ let instance;
+ let disposableClass;
+
+ beforeEach(() => {
+ instance = new Disposable();
+
+ disposableClass = {
+ dispose: jasmine.createSpy('dispose'),
+ };
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('add', () => {
+ it('adds disposable classes', () => {
+ instance.add(disposableClass);
+
+ expect(instance.disposers.size).toBe(1);
+ });
+ });
+
+ describe('dispose', () => {
+ beforeEach(() => {
+ instance.add(disposableClass);
+ });
+
+ it('calls dispose on all cached disposers', () => {
+ instance.dispose();
+
+ expect(disposableClass.dispose).toHaveBeenCalled();
+ });
+
+ it('clears cached disposers', () => {
+ instance.dispose();
+
+ expect(instance.disposers.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
new file mode 100644
index 00000000000..8c134f178c0
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_manager_spec.js
@@ -0,0 +1,81 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import ModelManager from '~/repo/lib/common/model_manager';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model manager', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = new ModelManager(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ describe('addModel', () => {
+ it('caches model', () => {
+ instance.addModel(file());
+
+ expect(instance.models.size).toBe(1);
+ });
+
+ it('caches model by file path', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.models.keys().next().value).toBe('path-name');
+ });
+
+ it('adds model into disposable', () => {
+ spyOn(instance.disposable, 'add').and.callThrough();
+
+ instance.addModel(file());
+
+ expect(instance.disposable.add).toHaveBeenCalled();
+ });
+
+ it('returns cached model', () => {
+ spyOn(instance.models, 'get').and.callThrough();
+
+ instance.addModel(file());
+ instance.addModel(file());
+
+ expect(instance.models.get).toHaveBeenCalled();
+ });
+ });
+
+ describe('hasCachedModel', () => {
+ it('returns false when no models exist', () => {
+ expect(instance.hasCachedModel('path')).toBeFalsy();
+ });
+
+ it('returns true when model exists', () => {
+ instance.addModel(file('path-name'));
+
+ expect(instance.hasCachedModel('path-name')).toBeTruthy();
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached models', () => {
+ instance.addModel(file());
+
+ instance.dispose();
+
+ expect(instance.models.size).toBe(0);
+ });
+
+ it('calls disposable dispose', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
new file mode 100644
index 00000000000..d41ade237ca
--- /dev/null
+++ b/spec/javascripts/repo/lib/common/model_spec.js
@@ -0,0 +1,84 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library model', () => {
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ });
+
+ it('creates original model & new model', () => {
+ expect(model.originalModel).not.toBeNull();
+ expect(model.model).not.toBeNull();
+ });
+
+ describe('path', () => {
+ it('returns file path', () => {
+ expect(model.path).toBe('path');
+ });
+ });
+
+ describe('getModel', () => {
+ it('returns model', () => {
+ expect(model.getModel()).toBe(model.model);
+ });
+ });
+
+ describe('getOriginalModel', () => {
+ it('returns original model', () => {
+ expect(model.getOriginalModel()).toBe(model.originalModel);
+ });
+ });
+
+ describe('onChange', () => {
+ it('caches event by path', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+ expect(model.events.keys().next().value).toBe('path');
+ });
+
+ it('calls callback on change', (done) => {
+ const spy = jasmine.createSpy();
+ model.onChange(spy);
+
+ model.getModel().setValue('123');
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
+ done();
+ });
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(model.disposable, 'dispose').and.callThrough();
+
+ model.dispose();
+
+ expect(model.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('clears events', () => {
+ model.onChange(() => {});
+
+ expect(model.events.size).toBe(1);
+
+ model.dispose();
+
+ expect(model.events.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
new file mode 100644
index 00000000000..2e32e8fa0bd
--- /dev/null
+++ b/spec/javascripts/repo/lib/decorations/controller_spec.js
@@ -0,0 +1,120 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import Model from '~/repo/lib/common/model';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library decorations controller', () => {
+ let editorInstance;
+ let controller;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ controller = new DecorationsController(editorInstance);
+ model = new Model(monaco, file('path'));
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ model.dispose();
+ editorInstance.dispose();
+ controller.dispose();
+ });
+
+ describe('getAllDecorationsForModel', () => {
+ it('returns empty array when no decorations exist for model', () => {
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations).toEqual([]);
+ });
+
+ it('returns decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ const decorations = controller.getAllDecorationsForModel(model);
+
+ expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
+ });
+ });
+
+ describe('addDecorations', () => {
+ it('caches decorations in a new map', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('does not create new cache model', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorations.size).toBe(1);
+ expect(controller.decorations.keys().next().value).toBe('path');
+ });
+
+ it('calls decorate method', () => {
+ spyOn(controller, 'decorate');
+
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ expect(controller.decorate).toHaveBeenCalled();
+ });
+ });
+
+ describe('decorate', () => {
+ it('sets decorations on editor instance', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations');
+
+ controller.decorate(model);
+
+ expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
+ });
+
+ it('caches decorations', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.size).toBe(1);
+ });
+
+ it('caches decorations by model URL', () => {
+ spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
+
+ controller.decorate(model);
+
+ expect(controller.editorDecorations.keys().next().value).toBe('path');
+ });
+ });
+
+ describe('dispose', () => {
+ it('clears cached decorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.decorations.size).toBe(0);
+ });
+
+ it('clears cached editorDecorations', () => {
+ controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+
+ controller.dispose();
+
+ expect(controller.editorDecorations.size).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
new file mode 100644
index 00000000000..ed62e28d3a3
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/controller_spec.js
@@ -0,0 +1,176 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import ModelManager from '~/repo/lib/common/model_manager';
+import DecorationsController from '~/repo/lib/decorations/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller';
+import { computeDiff } from '~/repo/lib/diff/diff';
+import { file } from '../../helpers';
+
+describe('Multi-file editor library dirty diff controller', () => {
+ let editorInstance;
+ let controller;
+ let modelManager;
+ let decorationsController;
+ let model;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ editorInstance = editor.create(monaco);
+ editorInstance.createInstance(document.createElement('div'));
+
+ modelManager = new ModelManager(monaco);
+ decorationsController = new DecorationsController(editorInstance);
+
+ model = modelManager.addModel(file());
+
+ controller = new DirtyDiffController(modelManager, decorationsController);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ controller.dispose();
+ model.dispose();
+ decorationsController.dispose();
+ editorInstance.dispose();
+ });
+
+ describe('getDiffChangeType', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(getDiffChangeType(change)).toBe(type);
+ });
+ });
+ });
+
+ describe('getDecorator', () => {
+ ['added', 'removed', 'modified'].forEach((type) => {
+ it(`returns with linesDecorationsClassName for ${type}`, () => {
+ const change = {
+ [type]: true,
+ };
+
+ expect(
+ getDecorator(change).options.linesDecorationsClassName,
+ ).toBe(`dirty-diff dirty-diff-${type}`);
+ });
+
+ it('returns with line numbers', () => {
+ const change = {
+ lineNumber: 1,
+ endLineNumber: 2,
+ [type]: true,
+ };
+
+ const range = getDecorator(change).range;
+
+ expect(range.startLineNumber).toBe(1);
+ expect(range.endLineNumber).toBe(2);
+ expect(range.startColumn).toBe(1);
+ expect(range.endColumn).toBe(1);
+ });
+ });
+ });
+
+ describe('attachModel', () => {
+ it('adds change event callback', () => {
+ spyOn(model, 'onChange');
+
+ controller.attachModel(model);
+
+ expect(model.onChange).toHaveBeenCalled();
+ });
+
+ it('calls throttledComputeDiff on change', () => {
+ spyOn(controller, 'throttledComputeDiff');
+
+ controller.attachModel(model);
+
+ model.getModel().setValue('123');
+
+ expect(controller.throttledComputeDiff).toHaveBeenCalled();
+ });
+ });
+
+ describe('computeDiff', () => {
+ it('posts to worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'postMessage');
+
+ controller.computeDiff(model);
+
+ expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
+ path: model.path,
+ originalContent: '',
+ newContent: '',
+ });
+ });
+ });
+
+ describe('reDecorate', () => {
+ it('calls decorations controller decorate', () => {
+ spyOn(controller.decorationsController, 'decorate');
+
+ controller.reDecorate(model);
+
+ expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('decorate', () => {
+ it('adds decorations into decorations controller', () => {
+ spyOn(controller.decorationsController, 'addDecorations');
+
+ controller.decorate({ data: { changes: [], path: 'path' } });
+
+ expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything());
+ });
+
+ it('adds decorations into editor', () => {
+ const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
+
+ controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } });
+
+ expect(spy).toHaveBeenCalledWith([], [{
+ range: new monaco.Range(
+ 1, 1, 1, 1,
+ ),
+ options: {
+ isWholeLine: true,
+ linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
+ },
+ }]);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposable dispose', () => {
+ spyOn(controller.disposable, 'dispose').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('terminates worker', () => {
+ spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
+ });
+
+ it('removes worker event listener', () => {
+ spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
+
+ controller.dispose();
+
+ expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything());
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..3269ec5d2c9
--- /dev/null
+++ b/spec/javascripts/repo/lib/diff/diff_spec.js
@@ -0,0 +1,80 @@
+import { computeDiff } from '~/repo/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it('', () => {
+ const diff = computeDiff('123', '1234')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ describe('added', () => {
+ it('', () => {
+ const diff = computeDiff('123', '123\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(3);
+ });
+ });
+
+ describe('removed', () => {
+ it('', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeTruthy();
+ });
+
+ it('', () => {
+ const diff = computeDiff('123\n123\n123', '123\n123')[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(2);
+ });
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
new file mode 100644
index 00000000000..b4887d063ed
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_options_spec.js
@@ -0,0 +1,7 @@
+import editorOptions from '~/repo/lib/editor_options';
+
+describe('Multi-file editor library editor options', () => {
+ it('returns an array', () => {
+ expect(editorOptions).toEqual(jasmine.any(Array));
+ });
+});
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
new file mode 100644
index 00000000000..cd32832a232
--- /dev/null
+++ b/spec/javascripts/repo/lib/editor_spec.js
@@ -0,0 +1,128 @@
+/* global monaco */
+import monacoLoader from '~/repo/monaco_loader';
+import editor from '~/repo/lib/editor';
+import { file } from '../helpers';
+
+describe('Multi-file editor library', () => {
+ let instance;
+
+ beforeEach((done) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ instance = editor.create(monaco);
+
+ done();
+ });
+ });
+
+ afterEach(() => {
+ instance.dispose();
+ });
+
+ it('creates instance of editor', () => {
+ expect(editor.editorInstance).not.toBeNull();
+ });
+
+ describe('createInstance', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ });
+
+ it('creates editor instance', () => {
+ spyOn(instance.monaco.editor, 'create').and.callThrough();
+
+ instance.createInstance(el);
+
+ expect(instance.monaco.editor.create).toHaveBeenCalled();
+ });
+
+ it('creates dirty diff controller', () => {
+ instance.createInstance(el);
+
+ expect(instance.dirtyDiffController).not.toBeNull();
+ });
+ });
+
+ describe('createModel', () => {
+ it('calls model manager addModel', () => {
+ spyOn(instance.modelManager, 'addModel');
+
+ instance.createModel('FILE');
+
+ expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
+ });
+ });
+
+ describe('attachModel', () => {
+ let model;
+
+ beforeEach(() => {
+ instance.createInstance(document.createElement('div'));
+
+ model = instance.createModel(file());
+ });
+
+ it('sets the current model on the instance', () => {
+ instance.attachModel(model);
+
+ expect(instance.currentModel).toBe(model);
+ });
+
+ it('attaches the model to the current instance', () => {
+ spyOn(instance.instance, 'setModel');
+
+ instance.attachModel(model);
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
+ });
+
+ it('attaches the model to the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'attachModel');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
+ });
+
+ it('re-decorates with the dirty diff controller', () => {
+ spyOn(instance.dirtyDiffController, 'reDecorate');
+
+ instance.attachModel(model);
+
+ expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
+ });
+ });
+
+ describe('clearEditor', () => {
+ it('resets the editor model', () => {
+ instance.createInstance(document.createElement('div'));
+
+ spyOn(instance.instance, 'setModel');
+
+ instance.clearEditor();
+
+ expect(instance.instance.setModel).toHaveBeenCalledWith(null);
+ });
+ });
+
+ describe('dispose', () => {
+ it('calls disposble dispose method', () => {
+ spyOn(instance.disposable, 'dispose').and.callThrough();
+
+ instance.dispose();
+
+ expect(instance.disposable.dispose).toHaveBeenCalled();
+ });
+
+ it('resets instance', () => {
+ instance.createInstance(document.createElement('div'));
+
+ expect(instance.instance).not.toBeNull();
+
+ instance.dispose();
+
+ expect(instance.instance).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
index 393a797c6a3..2bbc49d5a9f 100644
--- a/spec/javascripts/repo/stores/actions/tree_spec.js
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/repo/stores';
import service from '~/repo/services';
import { file, resetStore } from '../../helpers';
@@ -255,7 +256,7 @@ describe('Multi-file store tree actions', () => {
let row;
beforeEach(() => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
row = {
url: 'submoduleurl',
@@ -276,7 +277,7 @@ describe('Multi-file store tree actions', () => {
it('opens submodule URL', (done) => {
store.dispatch('clickedTreeRow', row)
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('submoduleurl');
done();
}).catch(done.fail);
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
index f2a7a698912..21d87e46216 100644
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import store from '~/repo/stores';
import service from '~/repo/services';
import { resetStore, file } from '../helpers';
@@ -10,11 +11,11 @@ describe('Multi-file store actions', () => {
describe('redirectToUrl', () => {
it('calls visitUrl', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
store.dispatch('redirectToUrl', 'test')
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('test');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
done();
})
@@ -326,13 +327,13 @@ describe('Multi-file store actions', () => {
});
it('redirects to new merge request page', (done) => {
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
store.dispatch('commitChanges', { payload, newMr: true })
.then(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
done();
}).catch(done.fail);
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 5505f983d71..72790eb215a 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -41,7 +41,7 @@ import '~/right_sidebar';
loadFixtures(fixtureName);
this.sidebar = new Sidebar;
$aside = $('.right-sidebar');
- $page = $('.page-with-sidebar');
+ $page = $('.layout-page');
$icon = $aside.find('i');
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index a2394857b82..206f95abc1a 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,8 +1,9 @@
/* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */
import '~/gl_dropdown';
-import '~/search_autocomplete';
+import SearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
+import * as urlUtils from '~/lib/utils/url_utility';
(function() {
var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -121,13 +122,13 @@ import '~/lib/utils/common_utils';
loadFixtures('static/search_autocomplete.html.raw');
// Prevent turbolinks from triggering within gl_dropdown
- spyOn(window.gl.utils, 'visitUrl').and.returnValue(true);
+ spyOn(urlUtils, 'visitUrl').and.returnValue(true);
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
- return widget = new gl.SearchAutocomplete;
+ return widget = new SearchAutocomplete();
});
afterEach(function() {
@@ -191,8 +192,6 @@ import '~/lib/utils/common_utils';
// browsers will not trigger default behavior (form submit, in this
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenTriggered();
- // Does a worse job at capturing the intent of the test, but works.
- expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
}).call(window);
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 0682b463043..3b094d20838 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -1,6 +1,6 @@
/* eslint-disable quote-props*/
-const sidebarMockData = {
+const RESPONSE_MAP = {
'GET': {
'/gitlab-org/gitlab-shell/issues/5.json': {
id: 45,
@@ -66,6 +66,65 @@ const sidebarMockData = {
},
labels: [],
},
+ '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar': {
+ assignees: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url: 'http: //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',
+ },
+ {
+ 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',
+ },
+ ],
+ human_time_estimate: null,
+ human_total_time_spent: null,
+ participants: [
+ {
+ name: 'User 0',
+ username: 'user0',
+ id: 22,
+ state: 'active',
+ avatar_url: 'http: //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',
+ },
+ {
+ 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',
+ },
+ ],
+ subscribed: true,
+ time_estimate: 0,
+ total_time_spent: 0,
+ },
'/autocomplete/projects?project_id=15': [
{
'id': 0,
@@ -113,9 +172,10 @@ const sidebarMockData = {
},
};
-export default {
+const mockData = {
+ responseMap: RESPONSE_MAP,
mediator: {
- endpoint: '/gitlab-org/gitlab-shell/issues/5.json',
+ endpoint: '/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar',
toggleSubscriptionEndpoint: '/gitlab-org/gitlab-shell/issues/5/toggle_subscription',
moveIssueEndpoint: '/gitlab-org/gitlab-shell/issues/5/move',
projectsAutocompleteEndpoint: '/autocomplete/projects?project_id=15',
@@ -141,12 +201,14 @@ export default {
name: 'Administrator',
username: 'root',
},
+};
- sidebarMockInterceptor(request, next) {
- const body = sidebarMockData[request.method.toUpperCase()][request.url];
+mockData.sidebarMockInterceptor = function (request, next) {
+ const body = this.responseMap[request.method.toUpperCase()][request.url];
- next(request.respondWith(JSON.stringify(body), {
- status: 200,
- }));
- },
-};
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200,
+ }));
+}.bind(mockData);
+
+export default mockData;
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index 929ba75e67d..b97e24d9dcf 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -4,20 +4,29 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import Mock from './mock_data';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('sidebar assignees', () => {
- let component;
- let SidebarAssigneeComponent;
+ let vm;
+ let mediator;
+ let sidebarAssigneesEl;
preloadFixtures('issues/open-issue.html.raw');
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
- spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough();
- spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough();
- this.mediator = new SidebarMediator(Mock.mediator);
+
loadFixtures('issues/open-issue.html.raw');
- this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+
+ mediator = new SidebarMediator(Mock.mediator);
+ spyOn(mediator, 'saveAssignees').and.callThrough();
+ spyOn(mediator, 'assignYourself').and.callThrough();
+
+ const SidebarAssigneeComponent = Vue.extend(SidebarAssignees);
+ sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
+ vm = mountComponent(SidebarAssigneeComponent, {
+ mediator,
+ field: sidebarAssigneesEl.dataset.field,
+ }, sidebarAssigneesEl);
});
afterEach(() => {
@@ -28,30 +37,24 @@ describe('sidebar assignees', () => {
});
it('calls the mediator when saves the assignees', () => {
- component = new SidebarAssigneeComponent()
- .$mount(this.sidebarAssigneesEl);
- component.saveAssignees();
-
- expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled();
+ vm.saveAssignees();
+ expect(mediator.saveAssignees).toHaveBeenCalled();
});
it('calls the mediator when "assignSelf" method is called', () => {
- component = new SidebarAssigneeComponent()
- .$mount(this.sidebarAssigneesEl);
- component.assignSelf();
+ vm.assignSelf();
- expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled();
- expect(this.mediator.store.assignees.length).toEqual(1);
+ expect(mediator.assignYourself).toHaveBeenCalled();
+ expect(mediator.store.assignees.length).toEqual(1);
});
it('hides assignees until fetched', (done) => {
- component = new SidebarAssigneeComponent().$mount(this.sidebarAssigneesEl);
- const currentAssignee = this.sidebarAssigneesEl.querySelector('.value');
+ const currentAssignee = sidebarAssigneesEl.querySelector('.value');
expect(currentAssignee).toBe(null);
- component.store.isFetching.assignees = false;
+ vm.store.isFetching.assignees = false;
Vue.nextTick(() => {
- expect(component.$el.querySelector('.value')).toBeVisible();
+ expect(vm.$el.querySelector('.value')).toBeVisible();
done();
});
});
diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js
index 7deb1fd2118..9efd109b996 100644
--- a/spec/javascripts/sidebar/sidebar_mediator_spec.js
+++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import SidebarService from '~/sidebar/services/sidebar_service';
@@ -33,10 +34,29 @@ describe('Sidebar mediator', () => {
.catch(done.fail);
});
- it('fetches the data', () => {
- spyOn(this.mediator.service, 'get').and.callThrough();
- this.mediator.fetch();
- expect(this.mediator.service.get).toHaveBeenCalled();
+ it('fetches the data', (done) => {
+ const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'];
+ spyOn(this.mediator, 'processFetchedData').and.callThrough();
+
+ this.mediator.fetch()
+ .then(() => {
+ expect(this.mediator.processFetchedData).toHaveBeenCalledWith(mockData);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('processes fetched data', () => {
+ const mockData = Mock.responseMap.GET['/gitlab-org/gitlab-shell/issues/5.json?serializer=sidebar'];
+ this.mediator.processFetchedData(mockData);
+
+ expect(this.mediator.store.assignees).toEqual(mockData.assignees);
+ expect(this.mediator.store.humanTimeEstimate).toEqual(mockData.human_time_estimate);
+ expect(this.mediator.store.humanTotalTimeSpent).toEqual(mockData.human_total_time_spent);
+ expect(this.mediator.store.participants).toEqual(mockData.participants);
+ expect(this.mediator.store.subscribed).toEqual(mockData.subscribed);
+ expect(this.mediator.store.timeEstimate).toEqual(mockData.time_estimate);
+ expect(this.mediator.store.totalTimeSpent).toEqual(mockData.total_time_spent);
});
it('sets moveToProjectId', () => {
@@ -66,12 +86,12 @@ describe('Sidebar mediator', () => {
const moveToProjectId = 7;
this.mediator.store.setMoveToProjectId(moveToProjectId);
spyOn(this.mediator.service, 'moveIssue').and.callThrough();
- spyOn(gl.utils, 'visitUrl');
+ spyOn(urlUtils, 'visitUrl');
this.mediator.moveIssue()
.then(() => {
expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId);
- expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index 51dee64fb93..ea4eae1e23f 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -120,6 +120,12 @@ describe('Sidebar store', () => {
expect(this.store.isFetching.participants).toEqual(false);
});
+ it('sets loading state', () => {
+ this.store.setLoadingState('assignees', true);
+
+ expect(this.store.isLoading.assignees).toEqual(true);
+ });
+
it('set time tracking data', () => {
this.store.setTimeTrackingData(Mock.time);
expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate);
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
index 7adf22b0f1f..a6113cb0bae 100644
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -26,11 +26,14 @@ describe('Sidebar Subscriptions', function () {
});
it('calls the mediator toggleSubscription on event', () => {
- spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve());
- vm = mountComponent(SidebarSubscriptions, {});
+ const mediator = new SidebarMediator();
+ spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve());
+ vm = mountComponent(SidebarSubscriptions, {
+ mediator,
+ });
eventHub.$emit('toggleSubscription');
- expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled();
+ expect(mediator.toggleSubscription).toHaveBeenCalled();
});
});
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 946f98379ce..763a15e710b 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,44 +1,42 @@
/* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */
-import '~/syntax_highlight';
+import syntaxHighlight from '~/syntax_highlight';
-(function() {
- describe('Syntax Highlighter', function() {
- var stubUserColorScheme;
- stubUserColorScheme = function(value) {
- if (window.gon == null) {
- window.gon = {};
- }
- return window.gon.user_color_scheme = value;
- };
- describe('on a js-syntax-highlight element', function() {
- beforeEach(function() {
- return setFixtures('<div class="js-syntax-highlight"></div>');
- });
- return it('applies syntax highlighting', function() {
- stubUserColorScheme('monokai');
- $('.js-syntax-highlight').syntaxHighlight();
- return expect($('.js-syntax-highlight')).toHaveClass('monokai');
- });
+describe('Syntax Highlighter', function() {
+ var stubUserColorScheme;
+ stubUserColorScheme = function(value) {
+ if (window.gon == null) {
+ window.gon = {};
+ }
+ return window.gon.user_color_scheme = value;
+ };
+ describe('on a js-syntax-highlight element', function() {
+ beforeEach(function() {
+ return setFixtures('<div class="js-syntax-highlight"></div>');
});
- return describe('on a parent element', function() {
- beforeEach(function() {
- return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
- });
- it('applies highlighting to all applicable children', function() {
- stubUserColorScheme('monokai');
- $('.parent').syntaxHighlight();
- expect($('.parent, .foo')).not.toHaveClass('monokai');
- return expect($('.monokai').length).toBe(2);
- });
- return it('prevents an infinite loop when no matches exist', function() {
- var highlight;
- setFixtures('<div></div>');
- highlight = function() {
- return $('div').syntaxHighlight();
- };
- return expect(highlight).not.toThrow();
- });
+ return it('applies syntax highlighting', function() {
+ stubUserColorScheme('monokai');
+ syntaxHighlight($('.js-syntax-highlight'));
+ return expect($('.js-syntax-highlight')).toHaveClass('monokai');
});
});
-}).call(window);
+ return describe('on a parent element', function() {
+ beforeEach(function() {
+ return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
+ });
+ it('applies highlighting to all applicable children', function() {
+ stubUserColorScheme('monokai');
+ syntaxHighlight($('.parent'));
+ expect($('.parent, .foo')).not.toHaveClass('monokai');
+ return expect($('.monokai').length).toBe(2);
+ });
+ return it('prevents an infinite loop when no matches exist', function() {
+ var highlight;
+ setFixtures('<div></div>');
+ highlight = function() {
+ return syntaxHighlight($('div'));
+ };
+ return expect(highlight).not.toThrow();
+ });
+ });
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index fd7aa332d17..6897c991066 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -17,6 +17,12 @@ Vue.config.warnHandler = (msg, vm, trace) => {
fail(`${msg}${trace}`);
};
+let hasVueErrors = false;
+Vue.config.errorHandler = function (err) {
+ hasVueErrors = true;
+ fail(err);
+};
+
Vue.use(VueResource);
// enable test fixtures
@@ -72,7 +78,7 @@ testsContext.keys().forEach(function (path) {
describe('test errors', () => {
beforeAll((done) => {
- if (hasUnhandledPromiseRejections || hasVueWarnings) {
+ if (hasUnhandledPromiseRejections || hasVueWarnings || hasVueErrors) {
setTimeout(done, 1000);
} else {
done();
@@ -86,6 +92,10 @@ describe('test errors', () => {
it('has no Vue warnings', () => {
expect(hasVueWarnings).toBe(false);
});
+
+ it('has no Vue error', () => {
+ expect(hasVueErrors).toBe(false);
+ });
});
// if we're generating coverage reports, make sure to include all files so
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 7d3c9319a11..59e16f0786e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -1,3 +1,4 @@
+import * as urlUtils from '~/lib/utils/url_utility';
import Todos from '~/todos';
import '~/lib/utils/common_utils';
@@ -16,7 +17,7 @@ describe('Todos', () => {
it('opens the todo url', (done) => {
const todoLink = todoItem.dataset.url;
- spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ spyOn(urlUtils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(todoLink);
done();
});
@@ -31,7 +32,7 @@ describe('Todos', () => {
beforeEach(() => {
metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true });
- visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {});
+ visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {});
windowOpenSpy = spyOn(window, 'open').and.callFake(() => {});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
index 7ee998c8fce..db7d083065b 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import * as urlUtils from '~/lib/utils/url_utility';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
+import { getTimeago } from '~/lib/utils/datetime_utility';
const deploymentMockData = [
{
@@ -48,7 +50,7 @@ describe('MRWidgetDeployment', () => {
describe('formatDate', () => {
it('should work', () => {
- const readable = gl.utils.getTimeago().format(deployment.deployed_at);
+ const readable = getTimeago().format(deployment.deployed_at);
expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
});
});
@@ -108,13 +110,13 @@ describe('MRWidgetDeployment', () => {
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
- spyOn(gl.utils, 'visitUrl').and.returnValue(true);
+ spyOn(urlUtils, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
- expect(gl.utils.visitUrl).toHaveBeenCalledWith(url);
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js
new file mode 100644
index 00000000000..721f4044659
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/modal_spec.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import modal from '~/vue_shared/components/modal.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Modal', () => {
+ it('does not render a primary button if no primaryButtonLabel', () => {
+ const modalComponent = Vue.extend(modal);
+ const vm = mountComponent(modalComponent);
+
+ expect(vm.$el.querySelector('.js-primary-button')).toBeNull();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
index b4c1f70ed1e..b4fb568f1d4 100644
--- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import '~/lib/utils/datetime_utility';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
describe('Time ago with tooltip component', () => {
let TimeagoTooltip;
@@ -24,10 +24,10 @@ describe('Time ago with tooltip component', () => {
expect(vm.$el.tagName).toEqual('TIME');
expect(
vm.$el.getAttribute('data-original-title'),
- ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z'));
+ ).toEqual(formatDate('2017-05-08T14:57:39.781Z'));
expect(vm.$el.getAttribute('data-placement')).toEqual('top');
- const timeago = gl.utils.getTimeago();
+ const timeago = getTimeago();
expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z'));
});
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
new file mode 100644
index 00000000000..447d74d4e08
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Toggle Button', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(toggleButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('render output', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ name: 'foo',
+ });
+ });
+
+ it('renders input with provided name', () => {
+ expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ });
+
+ it('renders input with provided value', () => {
+ expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
+ });
+
+ it('renders Enabled and Disabled text data attributes', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled');
+ expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled');
+ });
+ });
+
+ describe('is-checked', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ });
+
+ spyOn(vm, '$emit');
+ });
+
+ it('renders is checked class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ });
+
+ it('emits change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ });
+ });
+
+ describe('is-disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ disabledInput: true,
+ });
+ spyOn(vm, '$emit');
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ });
+
+ it('does not emit change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('is-loading', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ isLoading: true,
+ });
+ });
+
+ it('renders loading class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index 59deca7757b..a547988d631 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -92,6 +92,27 @@ describe API::Helpers::Pagination do
subject.paginate(resource)
end
end
+
+ context 'if order' do
+ it 'is not present it adds default order(:id) if no order is present' do
+ resource.order_values = []
+
+ paginated_relation = subject.paginate(resource)
+
+ expect(resource.order_values).to be_empty
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.first).to be_ascending
+ expect(paginated_relation.order_values.first.expr.name).to eq :id
+ end
+
+ it 'is present it does not add anything' do
+ paginated_relation = subject.paginate(resource.order(created_at: :desc))
+
+ expect(paginated_relation.order_values).to be_present
+ expect(paginated_relation.order_values.first).to be_descending
+ expect(paginated_relation.order_values.first.expr.name).to eq :created_at
+ end
+ end
end
context 'when resource empty' do
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index b68301a066a..b68301a066a 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
new file mode 100644
index 00000000000..6ee3d531d6e
--- /dev/null
+++ b/spec/lib/backup/repository_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Backup::Repository do
+ let(:progress) { StringIO.new }
+ let!(:project) { create(:project) }
+
+ before do
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
+ end
+
+ describe '#dump' do
+ describe 'repo failure' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+ end
+
+ it 'does not raise error' do
+ expect { described_class.new.dump }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#restore' do
+ describe 'command failure' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+ end
+
+ it 'shows the appropriate error' do
+ described_class.new.restore
+
+ expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
+ end
+ end
+ end
+
+ describe '#empty_repo?' do
+ context 'for a wiki' do
+ let(:wiki) { create(:project_wiki) }
+
+ it 'invalidates the emptiness cache' do
+ expect(wiki.repository).to receive(:expire_emptiness_caches).once
+
+ wiki.empty?
+ end
+
+ context 'wiki repo has content' do
+ let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
+
+ it 'returns true, regardless of bad cache value' do
+ expect(described_class.new.send(:empty_repo?, wiki)).to be(false)
+ end
+ end
+
+ context 'wiki repo does not have content' do
+ it 'returns true, regardless of bad cache value' do
+ expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index d70749536b8..68ca960caab 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -3,20 +3,20 @@ require 'spec_helper'
describe Banzai::CrossProjectReference do
include described_class
- describe '#project_from_ref' do
+ describe '#parent_from_ref' do
context 'when no project was referenced' do
it 'returns the project from context' do
project = double
allow(self).to receive(:context).and_return({ project: project })
- expect(project_from_ref(nil)).to eq project
+ expect(parent_from_ref(nil)).to eq project
end
end
context 'when referenced project does not exist' do
it 'returns nil' do
- expect(project_from_ref('invalid/reference')).to be_nil
+ expect(parent_from_ref('invalid/reference')).to be_nil
end
end
@@ -27,7 +27,7 @@ describe Banzai::CrossProjectReference do
expect(Project).to receive(:find_by_full_path)
.with('cross/reference').and_return(project2)
- expect(project_from_ref('cross/reference')).to eq project2
+ expect(parent_from_ref('cross/reference')).to eq project2
end
end
end
diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
index 7c0ba9ee67f..1e82d18d056 100644
--- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
@@ -3,67 +3,67 @@ require 'spec_helper'
describe Banzai::Filter::AbstractReferenceFilter do
let(:project) { create(:project) }
- describe '#references_per_project' do
- it 'returns a Hash containing references grouped per project paths' do
+ describe '#references_per_parent' do
+ it 'returns a Hash containing references grouped per parent paths' do
doc = Nokogiri::HTML.fragment("#1 #{project.full_path}#2")
filter = described_class.new(doc, project: project)
expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
expect(filter).to receive(:object_sym).twice.and_return(:issue)
- refs = filter.references_per_project
+ refs = filter.references_per_parent
expect(refs).to be_an_instance_of(Hash)
expect(refs[project.full_path]).to eq(Set.new(%w[1 2]))
end
end
- describe '#projects_per_reference' do
- it 'returns a Hash containing projects grouped per project paths' do
+ describe '#parent_per_reference' do
+ it 'returns a Hash containing projects grouped per parent paths' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter).to receive(:references_per_project)
+ expect(filter).to receive(:references_per_parent)
.and_return({ project.full_path => Set.new(%w[1]) })
- expect(filter.projects_per_reference)
+ expect(filter.parent_per_reference)
.to eq({ project.full_path => project })
end
end
- describe '#find_projects_for_paths' do
+ describe '#find_for_paths' do
let(:doc) { Nokogiri::HTML.fragment('') }
let(:filter) { described_class.new(doc, project: project) }
context 'with RequestStore disabled' do
it 'returns a list of Projects for a list of paths' do
- expect(filter.find_projects_for_paths([project.full_path]))
+ expect(filter.find_for_paths([project.full_path]))
.to eq([project])
end
it "return an empty array for paths that don't exist" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
end
context 'with RequestStore enabled', :request_store do
it 'returns a list of Projects for a list of paths' do
- expect(filter.find_projects_for_paths([project.full_path]))
+ expect(filter.find_for_paths([project.full_path]))
.to eq([project])
end
context "when no project with that path exists" do
it "returns no value" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
it "adds the ref to the project refs cache" do
project_refs_cache = {}
- allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache)
+ allow(filter).to receive(:refs_cache).and_return(project_refs_cache)
- filter.find_projects_for_paths(['nonexistent/project'])
+ filter.find_for_paths(['nonexistent/project'])
expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
end
@@ -71,11 +71,11 @@ describe Banzai::Filter::AbstractReferenceFilter do
context 'when the project refs cache includes nil values' do
before do
# adds { 'nonexistent/project' => nil } to cache
- filter.project_from_ref_cached('nonexistent/project')
+ filter.from_ref_cached('nonexistent/project')
end
it "return an empty array for paths that don't exist" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
end
@@ -83,12 +83,12 @@ describe Banzai::Filter::AbstractReferenceFilter do
end
end
- describe '#current_project_path' do
- it 'returns the path of the current project' do
+ describe '#current_parent_path' do
+ it 'returns the path of the current parent' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter.current_project_path).to eq(project.full_path)
+ expect(filter.current_parent_path).to eq(project.full_path)
end
end
end
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 702fcac0c6f..080a5f57da9 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -92,6 +92,18 @@ describe Banzai::Filter::CommitReferenceFilter do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.project_commit_url(project, reference, only_path: true)
end
+
+ context "in merge request context" do
+ let(:noteable) { create(:merge_request, target_project: project, source_project: project) }
+ let(:commit) { noteable.commits.first }
+
+ it 'handles merge request contextual commit references' do
+ url = urls.diffs_project_merge_request_url(project, noteable, commit_id: commit.id)
+ doc = reference_filter("See #{reference}", noteable: noteable)
+
+ expect(doc.css('a').first[:href]).to eq(url)
+ end
+ end
end
context 'cross-project / cross-namespace complete reference' do
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index f70c69ef588..3a5f52ea23f 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -157,6 +157,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.full_path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -201,6 +207,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -245,6 +257,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -269,8 +287,15 @@ 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>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'cross-project reference in link href' do
@@ -291,8 +316,15 @@ 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>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference_link}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'cross-project URL in link href' do
@@ -313,8 +345,15 @@ 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>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference_link}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'group context' do
@@ -387,19 +426,19 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
- describe '#issues_per_project' do
+ describe '#records_per_parent' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter).to receive(:projects_per_reference)
+ expect(filter).to receive(:parent_per_reference)
.and_return({ project.full_path => project })
- expect(filter).to receive(:references_per_project)
+ expect(filter).to receive(:references_per_parent)
.and_return({ project.full_path => Set.new([issue.iid]) })
- expect(filter.issues_per_project)
+ expect(filter.records_per_parent)
.to eq({ project => { issue.iid => issue } })
end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 85eddde732e..0cfef4ff5bf 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -65,6 +65,13 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
+ it 'prepends a prefix to digits-only ids' do
+ doc = filter(header(1, "123") + header(2, "1.0"))
+
+ expect(doc.css('h1 a').first.attr('href')).to eq '#anchor-123'
+ expect(doc.css('h2 a').first.attr('href')).to eq '#anchor-10'
+ end
+
it 'supports Unicode' do
doc = filter(header(1, '한글'))
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 60a88e903ef..76bc0c36ab7 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -89,7 +89,35 @@ describe Banzai::Filter::UploadLinkFilter do
end
end
- context 'when project context does not exist' do
+ context 'in group context' do
+ let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
+ let(:group) { create(:group) }
+ let(:filter_context) { { project: nil, group: group } }
+ let(:relative_path) { "groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
+
+ it 'rewrites the link correctly' do
+ doc = raw_filter(upload_link, filter_context)
+
+ expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
+ end
+
+ it 'rewrites the link correctly for subgroup' do
+ subgroup = create(:group, parent: group)
+ relative_path = "groups/#{subgroup.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = raw_filter(upload_link, { project: nil, group: subgroup })
+
+ expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'), filter_context)
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+ end
+
+ context 'when project or group context does not exist' do
let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
it 'does not raise error' do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 23dbe2b6238..4cef3bdb24b 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -70,12 +70,12 @@ describe Banzai::ReferenceParser::IssueParser do
end
end
- describe '#issues_for_nodes' do
+ describe '#records_for_nodes' do
it 'returns a Hash containing the issues for a list of nodes' do
link['data-issue'] = issue.id.to_s
nodes = [link]
- expect(subject.issues_for_nodes(nodes)).to eq({ link => issue })
+ expect(subject.records_for_nodes(nodes)).to eq({ link => issue })
end
end
end
diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
index 79d2c071446..e1c4f9cfea7 100644
--- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb
@@ -2,19 +2,20 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
+ let(:projects) { table(:projects) }
- let(:base1) { create(:project) }
- let(:base1_fork1) { create(:project) }
- let(:base1_fork2) { create(:project) }
+ let(:base1) { projects.create }
+ let(:base1_fork1) { projects.create }
+ let(:base1_fork2) { projects.create }
- let(:base2) { create(:project) }
- let(:base2_fork1) { create(:project) }
- let(:base2_fork2) { create(:project) }
+ let(:base2) { projects.create }
+ let(:base2_fork1) { projects.create }
+ let(:base2_fork2) { projects.create }
- let(:fork_of_fork) { create(:project) }
- let(:fork_of_fork2) { create(:project) }
- let(:second_level_fork) { create(:project) }
- let(:third_level_fork) { create(:project) }
+ let(:fork_of_fork) { projects.create }
+ let(:fork_of_fork2) { projects.create }
+ let(:second_level_fork) { projects.create }
+ let(:third_level_fork) { projects.create }
let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) }
let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) }
@@ -97,7 +98,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
end
it 'does not miss members for forks of forks for which the root was deleted' do
- forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: projects.create.id)
base1.destroy
expect(migration.missing_members?(7, 10)).to be_falsy
@@ -105,8 +106,8 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat
context 'with more forks' do
before do
- forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
- forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id)
+ forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id)
+ forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id)
end
it 'only processes a single batch of links at a time' do
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
index cb52d971047..3998ca940a4 100644
--- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -225,7 +225,8 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati
let(:user_class) { table(:users) }
let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
let(:namespace) { create(:namespace, owner: author) }
- let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
+ let(:projects) { table(:projects) }
+ let(:project) { projects.create(namespace_id: namespace.id, creator_id: author.id) }
# We can not rely on FactoryGirl as the state of Event may change in ways that
# the background migration does not expect, hence we use the Event class of
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index e52baf8dde7..8582af96199 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do
let(:migration) { described_class.new }
- let(:base1) { create(:project) }
+ let(:projects) { table(:projects) }
+ let(:base1) { projects.create }
- let(:base2) { create(:project) }
- let(:base2_fork1) { create(:project) }
+ let(:base2) { projects.create }
+ let(:base2_fork1) { projects.create }
let!(:forked_project_links) { table(:forked_project_links) }
let!(:fork_networks) { table(:fork_networks) }
@@ -18,10 +19,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# A normal fork link
forked_project_links.create(id: 1,
forked_from_project_id: base1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 2,
forked_from_project_id: base1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 3,
forked_from_project_id: base2.id,
forked_to_project_id: base2_fork1.id)
@@ -29,10 +30,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
# create a fork of a fork
forked_project_links.create(id: 4,
forked_from_project_id: base2_fork1.id,
- forked_to_project_id: create(:project).id)
+ forked_to_project_id: projects.create.id)
forked_project_links.create(id: 5,
- forked_from_project_id: create(:project).id,
- forked_to_project_id: create(:project).id)
+ forked_from_project_id: projects.create.id,
+ forked_to_project_id: projects.create.id)
# Stub out the calls to the other migrations
allow(BackgroundMigrationWorker).to receive(:perform_in)
@@ -63,7 +64,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
end
it 'creates a fork network for the fork of which the source was deleted' do
- fork = create(:project)
+ fork = projects.create
forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id)
migration.perform(5, 8)
diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..be45c00dfe6
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
@@ -0,0 +1,519 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ subject { described_class.new }
+
+ let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
+ let!(:uploads) { described_class::Upload }
+
+ before do
+ DatabaseCleaner.clean
+ drop_temp_table_if_exists
+ ensure_temporary_tracking_table_exists
+ uploads.delete_all
+ end
+
+ after(:all) do
+ drop_temp_table_if_exists
+ end
+
+ context 'with untracked files and tracked files in untracked_files_for_uploads' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user1) { create(:user, :with_avatar) }
+ let!(:user2) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let!(:project2) { create(:project, :with_avatar) }
+
+ before do
+ UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload
+ UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload
+
+ # File records created by PrepareUntrackedUploads
+ untracked_files_for_uploads.create!(path: appearance.uploads.first.path)
+ untracked_files_for_uploads.create!(path: appearance.uploads.last.path)
+ untracked_files_for_uploads.create!(path: user1.uploads.first.path)
+ untracked_files_for_uploads.create!(path: user2.uploads.first.path)
+ untracked_files_for_uploads.create!(path: project1.uploads.first.path)
+ untracked_files_for_uploads.create!(path: project2.uploads.first.path)
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}")
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}")
+
+ # Untrack 4 files
+ user2.uploads.delete_all
+ project2.uploads.delete_all # 2 files: avatar and a Markdown upload
+ appearance.uploads.where("path like '%header_logo%'").delete_all
+ end
+
+ it 'adds untracked files to the uploads table' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { uploads.count }.from(4).to(8)
+
+ expect(user2.uploads.count).to eq(1)
+ expect(project2.uploads.count).to eq(2)
+ expect(appearance.uploads.count).to eq(2)
+ end
+
+ it 'deletes rows after processing them' do
+ expect(subject).to receive(:drop_temp_table_if_finished) # Don't drop the table so we can look at it
+
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { untracked_files_for_uploads.count }.from(8).to(0)
+ end
+
+ it 'does not create duplicate uploads of already tracked files' do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(project1.uploads.count).to eq(2)
+ expect(appearance.uploads.count).to eq(2)
+ end
+
+ it 'uses the start and end batch ids [only 1st half]' do
+ ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
+ start_id = ids[0]
+ end_id = ids[3]
+
+ expect do
+ subject.perform(start_id, end_id)
+ end.to change { uploads.count }.from(4).to(6)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(user2.uploads.count).to eq(1)
+ expect(appearance.uploads.count).to eq(2)
+ expect(project1.uploads.count).to eq(2)
+ expect(project2.uploads.count).to eq(0)
+
+ # Only 4 have been either confirmed or added to uploads
+ expect(untracked_files_for_uploads.count).to eq(4)
+ end
+
+ it 'uses the start and end batch ids [only 2nd half]' do
+ ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
+ start_id = ids[4]
+ end_id = ids[7]
+
+ expect do
+ subject.perform(start_id, end_id)
+ end.to change { uploads.count }.from(4).to(6)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(user2.uploads.count).to eq(0)
+ expect(appearance.uploads.count).to eq(1)
+ expect(project1.uploads.count).to eq(2)
+ expect(project2.uploads.count).to eq(2)
+
+ # Only 4 have been either confirmed or added to uploads
+ expect(untracked_files_for_uploads.count).to eq(4)
+ end
+
+ it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do
+ subject.perform(1, untracked_files_for_uploads.last.id - 1)
+
+ expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy
+ end
+
+ it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey
+ end
+
+ it 'does not block a whole batch because of one bad path' do
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a")
+ expect(untracked_files_for_uploads.count).to eq(9)
+ expect(uploads.count).to eq(4)
+
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(untracked_files_for_uploads.count).to eq(1)
+ expect(uploads.count).to eq(8)
+ end
+
+ it 'an unparseable path is shown in error output' do
+ bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a"
+ untracked_files_for_uploads.create!(path: bad_path)
+
+ expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/)
+
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end
+ end
+
+ context 'with no untracked files' do
+ it 'does not add to the uploads table (and does not raise error)' do
+ expect do
+ subject.perform(1, 1000)
+ end.not_to change { uploads.count }.from(0)
+ end
+ end
+
+ describe 'upload outcomes for each path pattern' do
+ shared_examples_for 'non_markdown_file' do
+ let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') }
+ let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
+
+ before do
+ model.uploads.delete_all
+ end
+
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { model.reload.uploads.count }.from(0).to(1)
+
+ expect(model.uploads.first.attributes).to include(expected_upload_attrs)
+ end
+ end
+
+ context 'for an appearance logo file path' do
+ let(:model) { create_or_update_appearance(logo: uploaded_file) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for an appearance header_logo file path' do
+ let(:model) { create_or_update_appearance(header_logo: uploaded_file) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ let(:model) { create(:note, :with_attachment) }
+ let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') }
+ let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
+
+ before do
+ Upload.where(model_type: 'Note', model_id: model.id).delete_all
+ end
+
+ # Can't use the shared example because Note doesn't have an `uploads` association
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1)
+
+ expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs)
+ end
+ end
+
+ context 'for a user avatar file path' do
+ let(:model) { create(:user, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a group avatar file path' do
+ let(:model) { create(:group, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a project avatar file path' do
+ let(:model) { create(:project, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ let(:model) { create(:project) }
+
+ before do
+ # Upload the file
+ UploadService.new(model, uploaded_file, FileUploader).execute
+
+ # Create the untracked_files_for_uploads record
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}")
+
+ # Save the expected upload attributes
+ @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum')
+
+ # Untrack the file
+ model.reload.uploads.delete_all
+ end
+
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { model.reload.uploads.count }.from(0).to(1)
+
+ expect(model.uploads.first.attributes).to include(@expected_upload_attrs)
+ end
+ end
+ end
+end
+
+describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do
+ include TrackUntrackedUploadsHelpers
+
+ let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload }
+
+ before(:all) do
+ ensure_temporary_tracking_table_exists
+ end
+
+ after(:all) do
+ drop_temp_table_if_exists
+ end
+
+ describe '#upload_path' do
+ def assert_upload_path(file_path, expected_upload_path)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.upload_path).to eq(expected_upload_path)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns the file path relative to the project directory in uploads' do
+ project = create(:project)
+ random_hex = SecureRandom.hex
+
+ assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg")
+ end
+ end
+ end
+
+ describe '#uploader' do
+ def assert_uploader(file_path, expected_uploader)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.uploader).to eq(expected_uploader)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns FileUploader as a string' do
+ project = create(:project)
+
+ assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader')
+ end
+ end
+ end
+
+ describe '#model_type' do
+ def assert_model_type(file_path, expected_model_type)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.model_type).to eq(expected_model_type)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns Appearance as a string' do
+ assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns Appearance as a string' do
+ assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns Note as a string' do
+ assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns User as a string' do
+ assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns Namespace as a string' do
+ assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns Project as a string' do
+ assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns Project as a string' do
+ project = create(:project)
+
+ assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project')
+ end
+ end
+ end
+
+ describe '#model_id' do
+ def assert_model_id(file_path, expected_model_id)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.model_id).to eq(expected_model_id)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1)
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1)
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234)
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns the ID as a string' do
+ project = create(:project)
+
+ assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id)
+ end
+ end
+ end
+
+ describe '#file_size' do
+ context 'for an appearance logo file path' do
+ let(:appearance) { create_or_update_appearance(logo: uploaded_file) }
+ let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) }
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(appearance.logo.size)
+ end
+ end
+
+ context 'for a project avatar file path' do
+ let(:project) { create(:project, avatar: uploaded_file) }
+ let(:untracked_file) { described_class.create!(path: project.uploads.first.path) }
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(project.avatar.size)
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ let(:project) { create(:project) }
+ let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") }
+
+ before do
+ UploadService.new(project, uploaded_file, FileUploader).execute
+ end
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(project.uploads.first.size)
+ end
+ end
+ end
+
+ def create_untracked_file(path_relative_to_upload_dir)
+ described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}")
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..cd3f1a45270
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
+
+ matcher :be_scheduled_migration do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ before do
+ DatabaseCleaner.clean
+
+ drop_temp_table_if_exists
+ end
+
+ after do
+ drop_temp_table_if_exists
+ end
+
+ around do |example|
+ # Especially important so the follow-up migration does not get run
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+
+ it 'ensures the untracked_files_for_uploads table exists' do
+ expect do
+ described_class.new.perform
+ end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true)
+ end
+
+ it 'has a path field long enough for really long paths' do
+ described_class.new.perform
+
+ component = 'a' * 255
+
+ long_path = [
+ 'uploads',
+ component, # project.full_path
+ component # filename
+ ].flatten.join('/')
+
+ record = untracked_files_for_uploads.create!(path: long_path)
+ expect(record.reload.path.size).to eq(519)
+ end
+
+ context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do
+ around do |example|
+ # If this is CI, we use Postgres 9.2 so this whole context should be
+ # skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
+ if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?)
+ example.run
+ end
+ end
+
+ context 'when files were uploaded before and after hashed storage was enabled' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let(:project2) { create(:project) } # instantiate after enabling hashed_storage
+
+ before do
+ # Markdown upload before enabling hashed_storage
+ UploadService.new(project1, uploaded_file, FileUploader).execute
+
+ stub_application_setting(hashed_storage_enabled: true)
+
+ # Markdown upload after enabling hashed_storage
+ UploadService.new(project2, uploaded_file, FileUploader).execute
+ end
+
+ it 'adds unhashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+
+ it 'adds files with paths relative to CarrierWave.root' do
+ described_class.new.perform
+ untracked_files_for_uploads.all.each do |file|
+ expect(file.path.start_with?('uploads/')).to be_truthy
+ end
+ end
+
+ it 'does not add hashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
+ expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
+ end
+
+ it 'correctly schedules the follow-up background migration jobs' do
+ described_class.new.perform
+
+ expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+
+ # E.g. from a previous failed run of this background migration
+ context 'when there is existing data in untracked_files_for_uploads' do
+ before do
+ described_class.new.perform
+ end
+
+ it 'does not error or produce duplicates of existing data' do
+ expect do
+ described_class.new.perform
+ end.not_to change { untracked_files_for_uploads.count }.from(5)
+ 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
+ end
+ end
+ end
+
+ context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
+ before do
+ # If this is CI, we use Postgres 9.2 so this stub has no effect.
+ #
+ # If this is being run on Postgres 9.5+ or MySQL, then this stub allows us
+ # to test the bulk insert functionality without ON CONFLICT DO NOTHING or
+ # IGNORE.
+ allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
+ end
+
+ context 'when files were uploaded before and after hashed storage was enabled' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let(:project2) { create(:project) } # instantiate after enabling hashed_storage
+
+ before do
+ # Markdown upload before enabling hashed_storage
+ UploadService.new(project1, uploaded_file, FileUploader).execute
+
+ stub_application_setting(hashed_storage_enabled: true)
+
+ # Markdown upload after enabling hashed_storage
+ UploadService.new(project2, uploaded_file, FileUploader).execute
+ end
+
+ it 'adds unhashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+
+ it 'adds files with paths relative to CarrierWave.root' do
+ described_class.new.perform
+ untracked_files_for_uploads.all.each do |file|
+ expect(file.path.start_with?('uploads/')).to be_truthy
+ end
+ end
+
+ it 'does not add hashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
+ expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
+ end
+
+ it 'correctly schedules the follow-up background migration jobs' do
+ described_class.new.perform
+
+ expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+
+ # E.g. from a previous failed run of this background migration
+ context 'when there is existing data in untracked_files_for_uploads' do
+ before do
+ described_class.new.perform
+ end
+
+ it 'does not error or produce duplicates of existing data' do
+ expect do
+ described_class.new.perform
+ end.not_to change { untracked_files_for_uploads.count }.from(5)
+ 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
+ end
+ end
+ end
+
+ # Very new or lightly-used installations that are running this migration
+ # may not have an upload directory because they have no uploads.
+ context 'when no files were ever uploaded' do
+ it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb
deleted file mode 100644
index 535cce12780..00000000000
--- a/spec/lib/gitlab/backup/repository_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-require 'spec_helper'
-
-describe Backup::Repository do
- let(:progress) { StringIO.new }
- let!(:project) { create(:project) }
-
- before do
- allow(progress).to receive(:puts)
- allow(progress).to receive(:print)
-
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
- allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
- end
-
- describe '#dump' do
- describe 'repo failure' do
- before do
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError)
- allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
- end
-
- it 'does not raise error' do
- expect { described_class.new.dump }.not_to raise_error
- end
-
- it 'shows the appropriate error' do
- described_class.new.dump
-
- expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError")
- end
- end
-
- describe 'command failure' do
- before do
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false)
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- end
-
- it 'shows the appropriate error' do
- described_class.new.dump
-
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
- end
- end
- end
-
- describe '#restore' do
- describe 'command failure' do
- before do
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- end
-
- it 'shows the appropriate error' do
- described_class.new.restore
-
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
- end
- end
- end
-
- describe '#empty_repo?' do
- context 'for a wiki' do
- let(:wiki) { create(:project_wiki) }
-
- context 'wiki repo has content' do
- let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
-
- before do
- wiki.repository.exists? # initial cache
- end
-
- context '`repository.exists?` is incorrectly cached as false' do
- before do
- repo = wiki.repository
- repo.send(:cache).expire(:exists?)
- repo.send(:cache).fetch(:exists?) { false }
- repo.send(:instance_variable_set, :@exists, false)
- end
-
- it 'returns false, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey
- end
- end
-
- context '`repository.exists?` is correctly cached as true' do
- it 'returns false' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey
- end
- end
- end
-
- context 'wiki repo does not have content' do
- context '`repository.exists?` is incorrectly cached as true' do
- before do
- repo = wiki.repository
- repo.send(:cache).expire(:exists?)
- repo.send(:cache).fetch(:exists?) { true }
- repo.send(:instance_variable_set, :@exists, true)
- end
-
- it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
- end
- end
-
- context '`repository.exists?` is correctly cached as false' do
- it 'returns true' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 7f3bf5fc41c..8a83e446935 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -132,6 +132,23 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do
expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git'))
end
+
+ it 'moves an existing project to the correct path' do
+ # This is a quick way to get a valid repository instead of copying an
+ # existing one. Since it's not persisted, the importer will try to
+ # create the project.
+ project = build(:project, :repository)
+ original_commit_count = project.repository.commit_count
+
+ bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path)
+ gitlab_importer = described_class.new(admin, bare_repo)
+
+ expect(gitlab_importer).to receive(:create_project).and_call_original
+
+ new_project = gitlab_importer.create_project_if_needed
+
+ expect(new_project.repository.commit_count).to eq(original_commit_count)
+ end
end
context 'with Wiki' do
diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
index 2db737f5fb6..61b73abcba4 100644
--- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb
@@ -46,6 +46,13 @@ describe ::Gitlab::BareRepositoryImport::Repository do
describe '#project_full_path' do
it 'returns the project full path' do
expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git')
+ expect(project_repo_path.project_full_path).to eq('to/repo')
+ end
+
+ it 'with no trailing slash in the root path' do
+ repo_path = described_class.new('/full/path', '/full/path/to/repo.git')
+
+ expect(repo_path.project_full_path).to eq('to/repo')
end
end
end
diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb
new file mode 100644
index 00000000000..fa1575e2177
--- /dev/null
+++ b/spec/lib/gitlab/checks/project_moved_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '.fetch_redirct_message' do
+ context 'with a redirect message queue' do
+ it 'should return the redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message)
+ end
+
+ it 'should delete the redirect message from redis' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil
+ described_class.fetch_redirect_message(user.id, project.id)
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
+ end
+ end
+
+ context 'with no redirect message queue' do
+ it 'should return nil' do
+ expect(described_class.fetch_redirect_message(1, 2)).to be_nil
+ end
+ end
+ end
+
+ describe '#add_redirect_message' do
+ it 'should queue a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.add_redirect_message).to eq("OK")
+ end
+ end
+
+ describe '#redirect_message' do
+ context 'when the push is rejected' do
+ it 'should return a redirect message telling the user to try again' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
+
+ expect(project_moved.redirect_message(rejected: true)).to eq(message)
+ end
+ end
+
+ context 'when the push is not rejected' do
+ it 'should return a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo}\n"
+
+ expect(project_moved.redirect_message).to eq(message)
+ end
+ end
+ end
+
+ describe '#permanent_redirect?' do
+ context 'with a permanent RedirectRoute' do
+ it 'should return true' do
+ project.route.create_redirect('foo/bar', permanent: true)
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_truthy
+ end
+ end
+
+ context 'without a permanent RedirectRoute' do
+ it 'should return false' do
+ project.route.create_redirect('foo/bar')
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
index 15eb01eb472..4884d5f8ba4 100644
--- a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
@@ -4,11 +4,24 @@ describe Gitlab::Ci::Build::Policy::Kubernetes do
let(:pipeline) { create(:ci_pipeline, project: project) }
context 'when kubernetes service is active' do
- set(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'is satisfied by a kubernetes pipeline' do
+ expect(described_class.new('active'))
+ .to be_satisfied_by(pipeline)
+ end
+ end
- it 'is satisfied by a kubernetes pipeline' do
- expect(described_class.new('active'))
- .to be_satisfied_by(pipeline)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
new file mode 100644
index 00000000000..3ae7053a995
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Build do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:pipeline) { Ci::Pipeline.new }
+
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'master',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
+ end
+
+ let(:step) { described_class.new(pipeline, command) }
+
+ before do
+ stub_repository_ci_yaml_file(sha: anything)
+ end
+
+ it 'never breaks the chain' do
+ step.perform!
+
+ expect(step.break?).to be false
+ end
+
+ it 'fills pipeline object with data' do
+ step.perform!
+
+ expect(pipeline.sha).not_to be_empty
+ expect(pipeline.sha).to eq project.commit.id
+ expect(pipeline.ref).to eq 'master'
+ expect(pipeline.tag).to be false
+ expect(pipeline.user).to eq user
+ expect(pipeline.project).to eq project
+ end
+
+ it 'sets a valid config source' do
+ step.perform!
+
+ expect(pipeline.repository_source?).to be true
+ end
+
+ it 'returns a valid pipeline' do
+ step.perform!
+
+ expect(pipeline).to be_valid
+ end
+
+ it 'does not persist a pipeline' do
+ step.perform!
+
+ expect(pipeline).not_to be_persisted
+ end
+
+ context 'when pipeline is running for a tag' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'mytag',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
+ end
+
+ before do
+ allow_any_instance_of(Repository).to receive(:tag_exists?).with('mytag').and_return(true)
+
+ step.perform!
+ end
+
+ it 'correctly indicated that this is a tagged pipeline' do
+ expect(pipeline).to be_tag
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
new file mode 100644
index 00000000000..75a177d2d1f
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Command do
+ set(:project) { create(:project, :repository) }
+
+ describe '#initialize' do
+ subject do
+ described_class.new(origin_ref: 'master')
+ end
+
+ it 'properly initialises object from hash' do
+ expect(subject.origin_ref).to eq('master')
+ end
+ end
+
+ context 'handling of origin_ref' do
+ let(:command) { described_class.new(project: project, origin_ref: origin_ref) }
+
+ describe '#branch_exists?' do
+ subject { command.branch_exists? }
+
+ context 'for existing branch' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid branch' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#tag_exists?' do
+ subject { command.tag_exists? }
+
+ context 'for existing ref' do
+ let(:origin_ref) { 'v1.0.0' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid ref' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#ref' do
+ subject { command.ref }
+
+ context 'for regular ref' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for branch ref' do
+ let(:origin_ref) { 'refs/heads/master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for tag ref' do
+ let(:origin_ref) { 'refs/tags/1.0.0' }
+
+ it { is_expected.to eq('1.0.0') }
+ end
+
+ context 'for other refs' do
+ let(:origin_ref) { 'refs/merge-requests/11/head' }
+
+ it { is_expected.to eq('refs/merge-requests/11/head') }
+ end
+ end
+ end
+
+ describe '#sha' do
+ subject { command.sha }
+
+ context 'when invalid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'returns empty value' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when a valid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: project.commit.id) }
+
+ it 'returns checkout_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid after_sha is specified' do
+ let(:command) { described_class.new(project: project, after_sha: project.commit.id) }
+
+ it 'returns after_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid origin_ref is specified' do
+ let(:command) { described_class.new(project: project, origin_ref: 'HEAD') }
+
+ it 'returns SHA for given ref' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+ end
+
+ describe '#origin_sha' do
+ subject { command.origin_sha }
+
+ context 'when using checkout_sha and after_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', after_sha: 'bbb') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when using after_sha only' do
+ let(:command) { described_class.new(project: project, after_sha: 'bbb') }
+
+ it 'uses after_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+ end
+
+ describe '#before_sha' do
+ subject { command.before_sha }
+
+ context 'when using checkout_sha and before_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', before_sha: 'bbb') }
+
+ it 'uses before_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+
+ context 'when using checkout_sha only' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when checkout_sha and before_sha are empty' do
+ let(:command) { described_class.new(project: project) }
+
+ it 'uses BLANK_SHA' do
+ is_expected.to eq(Gitlab::Git::BLANK_SHA)
+ end
+ end
+ end
+
+ describe '#protected_ref?' do
+ let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
+
+ subject { command.protected_ref? }
+
+ context 'when a ref is protected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a ref is unprotected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index f54e2326b06..1b03227d67b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
end
let(:command) do
- double('command', project: project,
- current_user: user,
- seeds_block: nil)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user, seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index e165e0fac2a..eca23694a2b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
- let(:command) { double('command' ) }
+ let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
index 32bd5de829b..dc13cae961c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:command) do
- double('command', project: project,
- current_user: user,
- ignore_skip_ci: false,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ ignore_skip_ci: false,
+ save_incompleted: true)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index 0bbdd23f4d6..a973ccda8de 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -5,11 +5,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
set(:user) { create(:user) }
let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: ref, project: project)
+ build_stubbed(:ci_pipeline, project: project)
end
let(:command) do
- double('command', project: project, current_user: user)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: ref)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 8357af38f92..5c12c6e6392 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
set(:user) { create(:user) }
let(:command) do
- double('command', project: project,
- current_user: user,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ save_incompleted: true)
end
let!(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
index bb356efe9ad..fb1b53fc55c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
@@ -3,10 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
-
- let(:command) do
- double('command', project: project, current_user: user)
- end
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
let!(:step) { described_class.new(pipeline, command) }
@@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
step.perform!
end
- context 'when pipeline ref and sha exists' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project)
+ context 'when ref and sha exists' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: project.commit.id)
end
it 'does not break the chain' do
@@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline ref does not exist' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'something', project: project)
+ context 'when ref does not exist' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'something')
end
it 'breaks the chain' do
@@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline does not have SHA set' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project)
+ context 'when does not have existing SHA set' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: 'something')
end
it 'breaks the chain' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index d72f8553f55..98880fe9f28 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -178,15 +178,29 @@ module Gitlab
end
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
- let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns seeds for kubernetes dependent job' do
+ seeds = subject.stage_seeds(pipeline)
- it 'returns seeds for kubernetes dependent job' do
- seeds = subject.stage_seeds(pipeline)
+ expect(seeds.size).to eq 2
+ expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
+ expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ end
+ end
- expect(seeds.size).to eq 2
- expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
- expect(seeds.second.builds.dig(0, :name)).to eq 'production'
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 3c8350b3aad..664ba0f7234 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -942,8 +942,8 @@ describe Gitlab::Database::MigrationHelpers do
end
it 'queues jobs in groups of buffer size 1' do
- expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]])
- expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]])
+ expect(BackgroundMigrationWorker).to receive(:bulk_perform_async).with([['FooJob', [id1, id2]]])
+ expect(BackgroundMigrationWorker).to receive(:bulk_perform_async).with([['FooJob', [id3, id3]]])
model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
end
@@ -960,8 +960,8 @@ describe Gitlab::Database::MigrationHelpers do
end
it 'queues jobs in bulk all at once (big buffer size)' do
- expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]],
- ['FooJob', [id3, id3]]])
+ expect(BackgroundMigrationWorker).to receive(:bulk_perform_async).with([['FooJob', [id1, id2]],
+ ['FooJob', [id3, id3]]])
model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index fcddfad3f9f..b2f13fae73f 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -73,6 +73,28 @@ describe Gitlab::Database do
end
end
+ describe '.replication_slots_supported?' do
+ it 'returns false when using MySQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.replication_slots_supported?).to eq(false)
+ end
+
+ it 'returns false when using PostgreSQL 9.3' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.3.1')
+
+ expect(described_class.replication_slots_supported?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 9.4.0 or newer' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.4.0')
+
+ expect(described_class.replication_slots_supported?).to eq(true)
+ end
+ end
+
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
@@ -199,6 +221,22 @@ describe Gitlab::Database do
described_class.bulk_insert('test', rows)
end
+ it 'does not quote values of a column in the disable_quote option' do
+ [1, 2, 4, 5].each do |i|
+ expect(connection).to receive(:quote).with(i)
+ end
+
+ described_class.bulk_insert('test', rows, disable_quote: :c)
+ end
+
+ it 'does not quote values of columns in the disable_quote option' do
+ [2, 5].each do |i|
+ expect(connection).to receive(:quote).with(i)
+ end
+
+ described_class.bulk_insert('test', rows, disable_quote: [:a, :c])
+ end
+
it 'handles non-UTF-8 data' do
expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 15451c2cf99..0a41362f606 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -31,6 +31,10 @@ describe Gitlab::Diff::InlineDiff do
expect(subject[7]).to eq([17..17])
expect(subject[8]).to be_nil
end
+
+ it 'can handle unchanged empty lines' do
+ expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error
+ end
end
describe "#inline_diffs" do
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
new file mode 100644
index 00000000000..dc1a93367a4
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::CreateMergeRequestHandler do
+ include_context :email_shared_context
+ it_behaves_like :reply_processing_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ after do
+ TestEnv.clean_test_path
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_new_merge_request.eml') }
+ let(:namespace) { create(:namespace, path: 'gitlabhq') }
+
+ let!(:project) { create(:project, :public, :repository, namespace: namespace, path: 'gitlabhq') }
+ let!(:user) do
+ create(
+ :user,
+ email: 'jake@adventuretime.ooo',
+ incoming_email_token: 'auth_token'
+ )
+ end
+
+ context "as a non-developer" do
+ before do
+ project.add_guest(user)
+ end
+
+ it "raises UserNotAuthorizedError if the user is not a member" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError)
+ end
+ end
+
+ context "as a developer" do
+ before do
+ project.add_developer(user)
+ end
+
+ context "when everything is fine" do
+ it "creates a new merge request" do
+ expect { receiver.execute }.to change { project.merge_requests.count }.by(1)
+ merge_request = project.merge_requests.last
+
+ expect(merge_request.author).to eq(user)
+ expect(merge_request.source_branch).to eq('feature')
+ expect(merge_request.title).to eq('Feature added')
+ expect(merge_request.description).to eq('Merge request description')
+ expect(merge_request.target_branch).to eq(project.default_branch)
+ end
+ end
+
+ context "something is wrong" do
+ context "when the merge request could not be saved" do
+ before do
+ allow_any_instance_of(MergeRequest).to receive(:save).and_return(false)
+ end
+
+ it "raises an InvalidMergeRequestError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError)
+ end
+ end
+
+ context "when we can't find the incoming_email_token" do
+ let(:email_raw) { fixture_file("emails/wrong_incoming_email_token.eml") }
+
+ it "raises an UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when the subject is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_merge_request_no_subject.eml") }
+
+ it "raises an InvalidMergeRequestError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError)
+ end
+ end
+
+ context "when the message body is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_merge_request_no_description.eml") }
+
+ it "creates a new merge request with description set from the last commit" do
+ expect { receiver.execute }.to change { project.merge_requests.count }.by(1)
+ merge_request = project.merge_requests.last
+
+ expect(merge_request.description).to eq('Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler_spec.rb b/spec/lib/gitlab/email/handler_spec.rb
new file mode 100644
index 00000000000..650b01c4df4
--- /dev/null
+++ b/spec/lib/gitlab/email/handler_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::Email::Handler do
+ describe '.for' do
+ it 'picks issue handler if there is not merge request prefix' do
+ expect(described_class.for('email', 'project+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
+ end
+
+ it 'picks merge request handler if there is merge request key' do
+ expect(described_class.for('email', 'project+merge-request+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateMergeRequestHandler)
+ end
+
+ it 'returns nil if no handler is found' do
+ expect(described_class.for('email', '')).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 9f4e3c49adc..5ed639543e0 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -278,6 +278,35 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
+ shared_examples '.shas_with_signatures' do
+ let(:signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e 570e7b2abdd848b95f2f578043fc23bd6f6fd24d] }
+ let(:unsigned_shas) { %w[19e2e9b4ef76b422ce1154af39a91323ccc57434 c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
+ let(:first_signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
+
+ it 'has 2 signed shas' do
+ ret = described_class.shas_with_signatures(repository, signed_shas)
+ expect(ret).to eq(signed_shas)
+ end
+
+ it 'has 0 signed shas' do
+ ret = described_class.shas_with_signatures(repository, unsigned_shas)
+ expect(ret).to eq([])
+ end
+
+ it 'has 1 signed sha' do
+ ret = described_class.shas_with_signatures(repository, first_signed_shas)
+ expect(ret).to contain_exactly(first_signed_shas.first)
+ end
+ end
+
+ describe '.shas_with_signatures with gitaly on' do
+ it_should_behave_like '.shas_with_signatures'
+ end
+
+ describe '.shas_with_signatures with gitaly disabled', :disable_gitaly do
+ it_should_behave_like '.shas_with_signatures'
+ end
+
describe '.find_all' do
shared_examples 'finding all commits' do
it 'should return a return a collection of commits' do
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
index 0506210887c..eb148cc3804 100644
--- a/spec/lib/gitlab/git/remote_repository_spec.rb
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
subject { described_class.new(repository) }
- describe '#empty_repo?' do
+ describe '#empty?' do
using RSpec::Parameterized::TableSyntax
where(:repository, :result) do
@@ -13,7 +13,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do
end
with_them do
- it { expect(subject.empty_repo?).to eq(result) }
+ it { expect(subject.empty?).to eq(result) }
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 2f49bd1bcf2..e6845420f7d 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -257,7 +257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#empty?' do
- it { expect(repository.empty?).to be_falsey }
+ it { expect(repository).not_to be_empty }
end
describe '#ref_names' do
@@ -588,12 +588,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_as_mirror_without_shell' do
+ describe '#fetch_repository_as_mirror' do
let(:new_repository) do
Gitlab::Git::Repository.new('default', 'my_project.git', '')
end
- subject { new_repository.fetch_as_mirror_without_shell(repository.path) }
+ subject { new_repository.fetch_repository_as_mirror(repository) }
before do
Gitlab::Shell.new.add_repository('default', 'my_project')
@@ -603,7 +603,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project')
end
- it 'fetches a url as a mirror remote' do
+ it 'fetches a repository as a mirror remote' do
subject
expect(refs(new_repository.path)).to eq(refs(repository.path))
@@ -1210,6 +1210,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ context 'when no root ref is available' do
+ it 'returns empty list' do
+ project = create(:project, :empty_repo)
+
+ names = project.repository.merged_branch_names(%w[feature])
+
+ expect(names).to be_empty
+ end
+ end
+
context 'when no branch names are specified' do
before do
repository.create_branch('identical', 'master')
@@ -1652,21 +1662,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe '#fetch_remote_without_shell' do
- let(:git_path) { Gitlab.config.git.bin_path }
- let(:remote_name) { 'my_remote' }
-
- subject { repository.fetch_remote_without_shell(remote_name) }
-
- it 'fetches the remote and returns true if the command was successful' do
- expect(repository).to receive(:popen)
- .with(%W(#{git_path} fetch #{remote_name}), repository.path, {})
- .and_return(['', 0])
-
- expect(subject).to be(true)
- end
- end
-
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
diff --git a/spec/lib/gitlab/git/storage/checker_spec.rb b/spec/lib/gitlab/git/storage/checker_spec.rb
new file mode 100644
index 00000000000..d74c3bcb04c
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/checker_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::Checker, :clean_gitlab_redis_shared_state do
+ let(:storage_name) { 'default' }
+ let(:hostname) { Gitlab::Environment.hostname }
+ let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+
+ subject(:checker) { described_class.new(storage_name) }
+
+ def value_from_redis(name)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, name)
+ end.first
+ end
+
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
+ describe '.check_all' do
+ it 'calls a check for each storage' do
+ fake_checker_default = double
+ fake_checker_broken = double
+ fake_logger = fake_logger
+
+ expect(described_class).to receive(:new).with('default', fake_logger) { fake_checker_default }
+ expect(described_class).to receive(:new).with('broken', fake_logger) { fake_checker_broken }
+ expect(fake_checker_default).to receive(:check_with_lease)
+ expect(fake_checker_broken).to receive(:check_with_lease)
+
+ described_class.check_all(fake_logger)
+ end
+
+ context 'with broken storage', :broken_storage do
+ it 'returns the results' do
+ expected_result = [
+ { storage: 'default', success: true },
+ { storage: 'broken', success: false }
+ ]
+
+ expect(described_class.check_all).to eq(expected_result)
+ end
+ end
+ end
+
+ describe '#initialize' do
+ it 'assigns the settings' do
+ expect(checker.hostname).to eq(hostname)
+ expect(checker.storage).to eq('default')
+ expect(checker.storage_path).to eq(TestEnv.repos_path)
+ end
+ end
+
+ describe '#check_with_lease' do
+ it 'only allows one check at a time' do
+ expect(checker).to receive(:check).once { sleep 1 }
+
+ thread = Thread.new { checker.check_with_lease }
+ checker.check_with_lease
+ thread.join
+ end
+
+ it 'returns a result hash' do
+ expect(checker.check_with_lease).to eq(storage: 'default', success: true)
+ end
+ end
+
+ describe '#check' do
+ it 'tracks that the storage was accessible' do
+ set_in_redis(:failure_count, 10)
+ set_in_redis(:last_failure, Time.now.to_f)
+
+ checker.check
+
+ expect(value_from_redis(:failure_count).to_i).to eq(0)
+ expect(value_from_redis(:last_failure)).to be_empty
+ expect(value_from_redis(:first_failure)).to be_empty
+ end
+
+ it 'calls the check with the correct arguments' do
+ stub_application_setting(circuitbreaker_storage_timeout: 30,
+ circuitbreaker_access_retries: 3)
+
+ expect(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3)
+ .and_call_original
+
+ checker.check
+ end
+
+ it 'returns `true`' do
+ expect(checker.check).to eq(true)
+ end
+
+ it 'maintains known storage keys' do
+ Timecop.freeze do
+ # Insert an old key to expire
+ old_entry = Time.now.to_i - 3.days.to_i
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed')
+ end
+
+ checker.check
+
+ known_keys = Gitlab::Git::Storage.redis.with do |redis|
+ redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1)
+ end
+
+ expect(known_keys).to contain_exactly(cache_key)
+ end
+ end
+
+ context 'the storage is not available', :broken_storage do
+ let(:storage_name) { 'broken' }
+
+ it 'tracks that the storage was inaccessible' do
+ Timecop.freeze do
+ expect { checker.check }.to change { value_from_redis(:failure_count).to_i }.by(1)
+
+ expect(value_from_redis(:last_failure)).not_to be_empty
+ expect(value_from_redis(:first_failure)).not_to be_empty
+ end
+ end
+
+ it 'returns `false`' do
+ expect(checker.check).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index 72dabca793a..210b90bfba9 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -1,11 +1,18 @@
require 'spec_helper'
-describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
+describe Gitlab::Git::Storage::CircuitBreaker, :broken_storage do
let(:storage_name) { 'default' }
let(:circuit_breaker) { described_class.new(storage_name, hostname) }
let(:hostname) { Gitlab::Environment.hostname }
let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
before do
# Override test-settings for the circuitbreaker with something more realistic
# for these specs.
@@ -19,35 +26,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
)
end
- def value_from_redis(name)
- Gitlab::Git::Storage.redis.with do |redis|
- redis.hmget(cache_key, name)
- end.first
- end
-
- def set_in_redis(name, value)
- Gitlab::Git::Storage.redis.with do |redis|
- redis.hmset(cache_key, name, value)
- end.first
- end
-
- describe '.reset_all!' do
- it 'clears all entries form redis' do
- set_in_redis(:failure_count, 10)
-
- described_class.reset_all!
-
- key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) }
-
- expect(key_exists).to be_falsey
- end
-
- it 'does not break when there are no keys in redis' do
- expect { described_class.reset_all! }.not_to raise_error
- end
- end
-
- describe '.for_storage' do
+ describe '.for_storage', :request_store do
it 'only builds a single circuitbreaker per storage' do
expect(described_class).to receive(:new).once.and_call_original
@@ -70,7 +49,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
it 'assigns the settings' do
expect(circuit_breaker.hostname).to eq(hostname)
expect(circuit_breaker.storage).to eq('default')
- expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path)
end
end
@@ -90,9 +68,9 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- describe '#failure_wait_time' do
+ describe '#check_interval' do
it 'reads the value from settings' do
- expect(circuit_breaker.failure_wait_time).to eq(1)
+ expect(circuit_breaker.check_interval).to eq(1)
end
end
@@ -113,12 +91,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(circuit_breaker.access_retries).to eq(4)
end
end
-
- describe '#backoff_threshold' do
- it 'reads the value from settings' do
- expect(circuit_breaker.backoff_threshold).to eq(5)
- end
- end
end
describe '#perform' do
@@ -133,19 +105,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- it 'raises the correct exception when backing off' do
- Timecop.freeze do
- set_in_redis(:last_failure, 1.second.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect { |b| circuit_breaker.perform(&b) }
- .to raise_error do |exception|
- expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing)
- expect(exception.retry_after).to eq(30)
- end
- end
- end
-
it 'yields the block' do
expect { |b| circuit_breaker.perform(&b) }
.to yield_control
@@ -169,36 +128,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
.to raise_error(Rugged::OSError)
end
- it 'tracks that the storage was accessible' do
- set_in_redis(:failure_count, 10)
- set_in_redis(:last_failure, Time.now.to_f)
-
- circuit_breaker.perform { '' }
-
- expect(value_from_redis(:failure_count).to_i).to eq(0)
- expect(value_from_redis(:last_failure)).to be_empty
- expect(circuit_breaker.failure_count).to eq(0)
- expect(circuit_breaker.last_failure).to be_nil
- end
-
- it 'only performs the accessibility check once' do
- expect(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).once.and_call_original
-
- 2.times { circuit_breaker.perform { '' } }
- end
-
- it 'calls the check with the correct arguments' do
- stub_application_setting(circuitbreaker_storage_timeout: 30,
- circuitbreaker_access_retries: 3)
-
- expect(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3)
- .and_call_original
-
- circuit_breaker.perform { '' }
- end
-
context 'with the feature disabled' do
before do
stub_feature_flags(git_storage_circuit_breaker: false)
@@ -221,31 +150,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(result).to eq('hello')
end
end
-
- context 'the storage is not available' do
- let(:storage_name) { 'broken' }
-
- it 'raises the correct exception' do
- expect(circuit_breaker).to receive(:track_storage_inaccessible)
-
- expect { circuit_breaker.perform { '' } }
- .to raise_error do |exception|
- expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible)
- expect(exception.retry_after).to eq(30)
- end
- end
-
- it 'tracks that the storage was inaccessible' do
- Timecop.freeze do
- expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible)
-
- expect(value_from_redis(:failure_count).to_i).to eq(1)
- expect(value_from_redis(:last_failure)).not_to be_empty
- expect(circuit_breaker.failure_count).to eq(1)
- expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now)
- end
- end
- end
end
describe '#circuit_broken?' do
@@ -264,32 +168,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- describe '#backing_off?' do
- it 'is true when there was a recent failure' do
- Timecop.freeze do
- set_in_redis(:last_failure, 1.second.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect(circuit_breaker.backing_off?).to be_truthy
- end
- end
-
- context 'the `failure_wait_time` is set to 0' do
- before do
- stub_application_setting(circuitbreaker_failure_wait_time: 0)
- end
-
- it 'is working even when there are failures' do
- Timecop.freeze do
- set_in_redis(:last_failure, 0.seconds.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect(circuit_breaker.backing_off?).to be_falsey
- end
- end
- end
- end
-
describe '#last_failure' do
it 'returns the last failure time' do
time = Time.parse("2017-05-26 17:52:30")
diff --git a/spec/lib/gitlab/git/storage/failure_info_spec.rb b/spec/lib/gitlab/git/storage/failure_info_spec.rb
new file mode 100644
index 00000000000..bae88fdda86
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/failure_info_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::FailureInfo, :broken_storage do
+ let(:storage_name) { 'default' }
+ let(:hostname) { Gitlab::Environment.hostname }
+ let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+
+ def value_from_redis(name)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, name)
+ end.first
+ end
+
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
+ describe '.reset_all!' do
+ it 'clears all entries form redis' do
+ set_in_redis(:failure_count, 10)
+
+ described_class.reset_all!
+
+ key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) }
+
+ expect(key_exists).to be_falsey
+ end
+
+ it 'does not break when there are no keys in redis' do
+ expect { described_class.reset_all! }.not_to raise_error
+ end
+ end
+
+ describe '.load' do
+ it 'loads failure information for a storage on a host' do
+ first_failure = Time.parse("2017-11-14 17:52:30")
+ last_failure = Time.parse("2017-11-14 18:54:37")
+ failure_count = 11
+
+ set_in_redis(:first_failure, first_failure.to_i)
+ set_in_redis(:last_failure, last_failure.to_i)
+ set_in_redis(:failure_count, failure_count.to_i)
+
+ info = described_class.load(cache_key)
+
+ expect(info.first_failure).to eq(first_failure)
+ expect(info.last_failure).to eq(last_failure)
+ expect(info.failure_count).to eq(failure_count)
+ end
+ end
+
+ describe '#no_failures?' do
+ it 'is true when there are no failures' do
+ info = described_class.new(nil, nil, 0)
+
+ expect(info.no_failures?).to be_truthy
+ end
+
+ it 'is false when there are failures' do
+ info = described_class.new(Time.parse("2017-11-14 17:52:30"),
+ Time.parse("2017-11-14 18:54:37"),
+ 20)
+
+ expect(info.no_failures?).to be_falsy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb
index 4a14a5201d1..bb670fc5d94 100644
--- a/spec/lib/gitlab/git/storage/health_spec.rb
+++ b/spec/lib/gitlab/git/storage/health_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
-describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do
+describe Gitlab::Git::Storage::Health, broken_storage: true do
let(:host1_key) { 'storage_accessible:broken:web01' }
let(:host2_key) { 'storage_accessible:default:kiq01' }
def set_in_redis(cache_key, value)
Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
redis.hmset(cache_key, :failure_count, value)
end.first
end
diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
index 5db37f55e03..93ad20011de 100644
--- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
end
describe '#failure_info' do
- it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } }
+ it { expect(breaker.failure_info.no_failures?).to be_falsy }
end
end
@@ -49,7 +49,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
end
describe '#failure_info' do
- it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) }
+ it { expect(breaker.failure_info.no_failures?).to be_truthy }
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c9643c5da47..2db560c2cec 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -193,7 +193,15 @@ describe Gitlab::GitAccess do
let(:actor) { build(:rsa_deploy_key_2048, user: user) }
end
- describe '#check_project_moved!' do
+ shared_examples 'check_project_moved' do
+ it 'enqueues a redirected message' do
+ push_access_check
+
+ expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil
+ end
+ end
+
+ describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
before do
project.add_master(user)
end
@@ -207,7 +215,40 @@ describe Gitlab::GitAccess do
end
end
- context 'when a redirect was followed to find the project' do
+ context 'when a permanent redirect and ssh protocol' do
+ let(:redirected_path) { 'some/other-path' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a permanent redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows_push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a temporal redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
it 'blocks push and pull access' do
@@ -219,16 +260,15 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
end
end
+ end
- context 'http protocol' do
- let(:protocol) { 'http' }
+ context 'with a temporal redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
- it 'includes the path to the project using HTTP' do
- aggregate_failures do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- end
- end
+ it 'does not allow to push and pull access' do
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
end
end
end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 494dfe0e595..ce15057dd7d 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -38,4 +38,29 @@ describe Gitlab::Git do
expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_Ã¥")
end
end
+
+ describe '.shas_eql?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sha1, :sha2, :result) do
+ sha = RepoHelpers.sample_commit.id
+ short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH]
+ too_short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH - 1]
+
+ [
+ [sha, sha, true],
+ [sha, short_sha, true],
+ [sha, sha.reverse, false],
+ [sha, too_short_sha, false],
+ [sha, nil, false]
+ ]
+ end
+
+ with_them do
+ it { expect(described_class.shas_eql?(sha1, sha2)).to eq(result) }
+ it 'is commutative' do
+ expect(described_class.shas_eql?(sha2, sha1)).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index cfaeb1f0d4f..0385dd762c2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -70,6 +70,10 @@ describe Gitlab::Identifier do
expect(identifier.identify_using_commit(project, '123')).to eq(user)
end
end
+
+ it 'returns nil if the project & ref are not present' do
+ expect(identifier.identify_using_commit(nil, nil)).to be_nil
+ end
end
describe '#identify_using_user' do
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 5341addf911..78767d06462 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -20,9 +20,39 @@ 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
+ around do |example|
+ Timecop.freeze do
+ example.run
+ 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
+ end
+
it 'observes the performance of the supplied block' do
expect(described_class.call_duration_histogram)
.to receive(:observe)
@@ -34,6 +64,8 @@ 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
diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
index 667e4747897..f66451c5188 100644
--- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb
@@ -21,7 +21,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
it 'samples various statistics' do
expect(sampler).to receive(:sample_memory_usage)
expect(sampler).to receive(:sample_file_descriptors)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
expect(sampler).to receive(:flush)
@@ -72,28 +71,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do
end
end
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler).to receive(:add_metric)
- .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash))
- .at_least(:once)
- .and_call_original
-
- sampler.sample_objects
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler).not_to receive(:add_metric)
- .with('object_counts', an_instance_of(Hash), type: nil)
-
- sampler.sample_objects
- end
- end
- end
-
describe '#sample_gc' do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 53699327da1..375cbf8a9ca 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do
it 'samples various statistics' do
expect(Gitlab::Metrics::System).to receive(:memory_usage)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
- expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
sampler.sample
@@ -65,26 +64,4 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
end
-
- if Gitlab::Metrics.mri?
- describe '#sample_objects' do
- it 'adds a metric containing the amount of allocated objects' do
- expect(sampler.metrics[:objects_total]).to receive(:set)
- .with(include(class: anything), be > 0)
- .at_least(:once)
- .and_call_original
-
- sampler.sample
- end
-
- it 'ignores classes without a name' do
- expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 })
-
- expect(sampler.metrics[:objects_total]).not_to receive(:set)
- .with(include(class: 'object_counts'), anything)
-
- sampler.sample
- end
- end
- end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 9c3e7d7e9ba..a424f0f5cfe 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -70,6 +70,15 @@ describe Gitlab::ProjectSearchResults do
subject { described_class.parse_search_result(search_result) }
+ it 'can correctly parse filenames including ":"' do
+ special_char_result = "\nmaster:testdata/project::function1.yaml-1----\nmaster:testdata/project::function1.yaml:2:test: data1\n"
+
+ blob = described_class.parse_search_result(special_char_result)
+
+ expect(blob.ref).to eq('master')
+ expect(blob.filename).to eq('testdata/project::function1.yaml')
+ end
+
it "returns a valid FoundBlob" do
is_expected.to be_an Gitlab::SearchResults::FoundBlob
expect(subject.id).to be_nil
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 476a3f1998d..8ec3f55e6de 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -115,6 +115,15 @@ describe Gitlab::ReferenceExtractor do
end
end
+ it 'does not include anchors from table of contents in issue references' do
+ issue1 = create(:issue, project: project)
+ issue2 = create(:issue, project: project)
+
+ subject.analyze("not real issue <h4>#{issue1.iid}</h4>, real issue #{issue2.to_reference}")
+
+ expect(subject.issues).to match_array([issue2])
+ end
+
it 'accesses valid issue objects' do
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
@@ -250,4 +259,34 @@ describe Gitlab::ReferenceExtractor do
subject { described_class.references_pattern }
it { is_expected.to be_kind_of Regexp }
end
+
+ describe 'referables prefixes' do
+ def prefixes
+ described_class::REFERABLES.each_with_object({}) do |referable, result|
+ klass = referable.to_s.camelize.constantize
+
+ next unless klass.respond_to?(:reference_prefix)
+
+ prefix = klass.reference_prefix
+ result[prefix] ||= []
+ result[prefix] << referable
+ end
+ end
+
+ it 'returns all supported prefixes' do
+ expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ &))
+ end
+
+ it 'does not allow one prefix for multiple referables if not allowed specificly' do
+ # make sure you are not overriding existing prefix before changing this hash
+ multiple_allowed = {
+ '@' => 3
+ }
+
+ prefixes.each do |prefix, referables|
+ expected_count = multiple_allowed[prefix] || 1
+ expect(referables.count).to eq(expected_count)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 2158b2837e2..eec6858a5de 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -200,18 +200,18 @@ describe Gitlab::Shell do
describe '#fork_repository' do
it 'returns true when the command succeeds' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'],
+ .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'],
nil, popen_vars).and_return([nil, 0])
- expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be true
+ expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be true
end
it 'return false when the command fails' do
expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'],
+ .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'],
nil, popen_vars).and_return(["error", 1])
- expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be false
+ expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be false
end
end
diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb
new file mode 100644
index 00000000000..0c66d764851
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_config_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+describe Gitlab::SidekiqConfig do
+ describe '.workers' do
+ it 'includes all workers' do
+ workers = described_class.workers
+
+ expect(workers).to include(PostReceive)
+ expect(workers).to include(MergeWorker)
+ end
+ end
+
+ describe '.worker_queues' do
+ it 'includes all queues' do
+ queues = described_class.worker_queues
+
+ expect(queues).to include('post_receive')
+ expect(queues).to include('merge')
+ expect(queues).to include('cronjob:stuck_import_jobs')
+ expect(queues).to include('mailers')
+ expect(queues).to include('default')
+ end
+ end
+
+ describe '.expand_queues' do
+ it 'expands queue namespaces to concrete queue names' do
+ queues = described_class.expand_queues(%w[cronjob])
+
+ expect(queues).to include('cronjob:stuck_import_jobs')
+ expect(queues).to include('cronjob:stuck_merge_jobs')
+ end
+
+ it 'lets concrete queue names pass through' do
+ queues = described_class.expand_queues(%w[post_receive])
+
+ expect(queues).to include('post_receive')
+ end
+
+ it 'lets unknown queues pass through' do
+ queues = described_class.expand_queues(%w[unknown])
+
+ expect(queues).to include('unknown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb
new file mode 100644
index 00000000000..7debf70a16f
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqVersioning::Manager do
+ before do
+ Sidekiq::Manager.prepend described_class
+ end
+
+ describe '#initialize' do
+ it 'listens on all expanded queues' do
+ manager = Sidekiq::Manager.new(queues: %w[post_receive repository_fork cronjob unknown])
+
+ queues = manager.options[:queues]
+
+ expect(queues).to include('post_receive')
+ expect(queues).to include('repository_fork')
+ expect(queues).to include('cronjob')
+ expect(queues).to include('cronjob:stuck_import_jobs')
+ expect(queues).to include('cronjob:stuck_merge_jobs')
+ expect(queues).to include('unknown')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb
new file mode 100644
index 00000000000..fa6d42e730d
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqVersioning, :sidekiq, :redis do
+ let(:foo_worker) do
+ Class.new do
+ def self.name
+ 'FooWorker'
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ let(:bar_worker) do
+ Class.new do
+ def self.name
+ 'BarWorker'
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ before do
+ allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker])
+ allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue])
+ end
+
+ describe '.install!' do
+ it 'prepends SidekiqVersioning::Manager into Sidekiq::Manager' do
+ described_class.install!
+
+ expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager)
+ end
+
+ it 'registers all versionless and versioned queues with Redis' do
+ described_class.install!
+
+ queues = Sidekiq::Queue.all.map(&:name)
+ expect(queues).to include('foo')
+ expect(queues).to include('bar')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/cli_spec.rb b/spec/lib/gitlab/storage_check/cli_spec.rb
new file mode 100644
index 00000000000..6db0925899c
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/cli_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::CLI do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, 1, false) }
+ subject(:runner) { described_class.new(options) }
+
+ describe '#update_settings' do
+ it 'updates the interval when changed in a valid response and logs the change' do
+ fake_response = double
+ expect(fake_response).to receive(:valid?).and_return(true)
+ expect(fake_response).to receive(:check_interval).and_return(42)
+ expect(runner.logger).to receive(:info)
+
+ runner.update_settings(fake_response)
+
+ expect(options.interval).to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb
new file mode 100644
index 00000000000..d869022fd31
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::GitlabCaller do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, nil, false) }
+ subject(:gitlab_caller) { described_class.new(options) }
+
+ describe '#call!' do
+ context 'when a socket is given' do
+ it 'calls a socket' do
+ fake_connection = double
+ expect(fake_connection).to receive(:post)
+ expect(Excon).to receive(:new).with('unix://tmp/socket.sock', socket: "tmp/socket.sock") { fake_connection }
+
+ gitlab_caller.call!
+ end
+ end
+
+ context 'when a host is given' do
+ let(:options) { Gitlab::StorageCheck::Options.new('http://localhost:8080', nil, nil, false) }
+
+ it 'it calls a http response' do
+ fake_connection = double
+ expect(Excon).to receive(:new).with('http://localhost:8080', socket: nil) { fake_connection }
+ expect(fake_connection).to receive(:post)
+
+ gitlab_caller.call!
+ end
+ end
+ end
+
+ describe '#headers' do
+ it 'Adds the JSON header' do
+ headers = gitlab_caller.headers
+
+ expect(headers['Content-Type']).to eq('application/json')
+ end
+
+ context 'when a token was provided' do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', 'atoken', nil, false) }
+
+ it 'adds it to the headers' do
+ expect(gitlab_caller.headers['TOKEN']).to eq('atoken')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/option_parser_spec.rb b/spec/lib/gitlab/storage_check/option_parser_spec.rb
new file mode 100644
index 00000000000..cad4dfbefcf
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/option_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::OptionParser do
+ describe '.parse!' do
+ it 'assigns all options' do
+ args = %w(--target unix://tmp/hello/world.sock --token thetoken --interval 42)
+
+ options = described_class.parse!(args)
+
+ expect(options.token).to eq('thetoken')
+ expect(options.interval).to eq(42)
+ expect(options.target).to eq('unix://tmp/hello/world.sock')
+ end
+
+ it 'requires the interval to be a number' do
+ args = %w(--target unix://tmp/hello/world.sock --interval fortytwo)
+
+ expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument)
+ end
+
+ it 'raises an error if the scheme is not included' do
+ args = %w(--target tmp/hello/world.sock)
+
+ expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument)
+ end
+
+ it 'raises an error if both socket and host are missing' do
+ expect { described_class.parse!([]) }.to raise_error(OptionParser::InvalidArgument)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/response_spec.rb b/spec/lib/gitlab/storage_check/response_spec.rb
new file mode 100644
index 00000000000..0ff2963e443
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/response_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::Response do
+ let(:fake_json) do
+ {
+ check_interval: 42,
+ results: [
+ { storage: 'working', success: true },
+ { storage: 'skipped', success: nil },
+ { storage: 'failing', success: false }
+ ]
+ }.to_json
+ end
+
+ let(:fake_http_response) do
+ fake_response = instance_double("Excon::Response - Status check")
+ allow(fake_response).to receive(:status).and_return(200)
+ allow(fake_response).to receive(:body).and_return(fake_json)
+ allow(fake_response).to receive(:headers).and_return('Content-Type' => 'application/json')
+
+ fake_response
+ end
+ let(:response) { described_class.new(fake_http_response) }
+
+ describe '#valid?' do
+ it 'is valid for a success response with parseable JSON' do
+ expect(response).to be_valid
+ end
+ end
+
+ describe '#check_interval' do
+ it 'returns the result from the JSON' do
+ expect(response.check_interval).to eq(42)
+ end
+ end
+
+ describe '#responsive_shards' do
+ it 'contains the names of working shards' do
+ expect(response.responsive_shards).to contain_exactly('working')
+ end
+ end
+
+ describe '#skipped_shards' do
+ it 'contains the names of skipped shards' do
+ expect(response.skipped_shards).to contain_exactly('skipped')
+ end
+ end
+
+ describe '#failing_shards' do
+ it 'contains the name of failing shards' do
+ expect(response.failing_shards).to contain_exactly('failing')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb
new file mode 100644
index 00000000000..4acf0334496
--- /dev/null
+++ b/spec/lib/gitlab/tcp_checker_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::TcpChecker do
+ before do
+ @server = TCPServer.new('localhost', 0)
+ _, @port, _, @ip = @server.addr
+ end
+
+ after do
+ @server.close
+ end
+
+ subject(:checker) { described_class.new(@ip, @port) }
+
+ describe '#check' do
+ subject { checker.check }
+
+ it 'can connect to an open port' do
+ is_expected.to be_truthy
+
+ expect(checker.error).to be_nil
+ end
+
+ it 'fails to connect to a closed port' do
+ @server.close
+
+ is_expected.to be_falsy
+
+ expect(checker.error).to be_a(Errno::ECONNREFUSED)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb
index 4a104ab6d97..473f8100771 100644
--- a/spec/lib/gitlab/utils/strong_memoize_spec.rb
+++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb
@@ -49,4 +49,16 @@ describe Gitlab::Utils::StrongMemoize do
end
end
end
+
+ describe '#clear_memoization' do
+ let(:value) { 'mepmep' }
+
+ it 'removes the instance variable' do
+ object.method_name
+
+ object.clear_memoization(:method_name)
+
+ expect(object.instance_variable_defined?(:@method_name)).to be(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 3137a72fdc4..e872a5290c5 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Utils do
- delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, to: :described_class
describe '.slugify' do
{
@@ -59,4 +59,12 @@ describe Gitlab::Utils do
expect(random_string).to be_kind_of(String)
end
end
+
+ describe '.which' do
+ it 'finds the full path to an executable binary' do
+ expect(File).to receive(:executable?).with('/bin/sh').and_return(true)
+
+ expect(which('sh', 'PATH' => '/bin')).to eq('/bin/sh')
+ end
+ end
end
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
index 70d2e22b48f..6120bafb2e3 100644
--- a/spec/lib/gitlab/view/presenter/factory_spec.rb
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do
expect(presenter).to be_a(Ci::BuildPresenter)
end
+
+ it 'uses the presenter_class if given on #initialize' do
+ MyCustomPresenter = Class.new(described_class)
+
+ presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate!
+
+ expect(presenter).to be_a(MyCustomPresenter)
+ end
end
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index fac23dce44d..ecb4034ec8b 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe GoogleApi::CloudPlatform::Client do
let(:token) { 'token' }
let(:client) { described_class.new(token, nil) }
+ let(:user_agent_options) { client.instance_eval { user_agent_header } }
describe '.session_key_for_redirect_uri' do
let(:state) { 'random_string' }
@@ -55,7 +56,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
- .to receive(:get_zone_cluster).and_return(gke_cluster)
+ .to receive(:get_zone_cluster).with(any_args, options: user_agent_options)
+ .and_return(gke_cluster)
end
it { is_expected.to eq(gke_cluster) }
@@ -74,7 +76,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
- .to receive(:create_cluster).and_return(operation)
+ .to receive(:create_cluster).with(any_args, options: user_agent_options)
+ .and_return(operation)
end
it { is_expected.to eq(operation) }
@@ -102,7 +105,8 @@ describe GoogleApi::CloudPlatform::Client do
before do
allow_any_instance_of(Google::Apis::ContainerV1::ContainerService)
- .to receive(:get_zone_operation).and_return(operation)
+ .to receive(:get_zone_operation).with(any_args, options: user_agent_options)
+ .and_return(operation)
end
it { is_expected.to eq(operation) }
@@ -125,4 +129,18 @@ describe GoogleApi::CloudPlatform::Client do
it { is_expected.to be_nil }
end
end
+
+ describe '#user_agent_header' do
+ subject { client.instance_eval { user_agent_header } }
+
+ it 'returns a RequestOptions object' do
+ expect(subject).to be_instance_of(Google::Apis::RequestOptions)
+ end
+
+ it 'has the correct GitLab version in User-Agent header' do
+ stub_const('Gitlab::VERSION', '10.3.0-pre')
+
+ expect(subject.header).to eq({ 'User-Agent': 'GitLab/10.3 (GPN:GitLab;)' })
+ end
+ end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index f942a22b6d1..4d0a3942996 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -342,6 +342,46 @@ describe Notify do
end
end
+ context 'for issue notes' do
+ let(:host) { Gitlab.config.gitlab.host }
+
+ context 'in discussion' do
+ set(:first_note) { create(:discussion_note_on_issue) }
+ set(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note) }
+ set(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note) }
+
+ subject { described_class.note_issue_email(recipient.id, third_note.id) }
+
+ it 'has In-Reply-To header pointing to previous note in discussion' do
+ expect(subject.header['In-Reply-To'].message_ids).to eq(["note_#{second_note.id}@#{host}"])
+ end
+
+ it 'has References header including the notes and issue of the discussion' do
+ expect(subject.header['References'].message_ids).to include("issue_#{first_note.noteable.id}@#{host}",
+ "note_#{first_note.id}@#{host}",
+ "note_#{second_note.id}@#{host}")
+ end
+
+ it 'has X-GitLab-Discussion-ID header' do
+ expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id)
+ end
+ end
+
+ context 'individual issue comments' do
+ set(:note) { create(:note_on_issue) }
+
+ subject { described_class.note_issue_email(recipient.id, note.id) }
+
+ it 'has In-Reply-To header pointing to the issue' do
+ expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"])
+ end
+
+ it 'has References header including the notes and issue of the discussion' do
+ expect(subject.header['References'].message_ids).to include("issue_#{note.noteable.id}@#{host}")
+ end
+ end
+ end
+
context 'for snippet notes' do
let(:project_snippet) { create(:project_snippet, project: project) }
let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) }
@@ -602,7 +642,7 @@ describe Notify do
it 'has the correct subject and body' do
aggregate_failures do
- is_expected.to have_subject("Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})")
+ is_expected.to have_subject("Re: #{project.name} | #{commit.title} (#{commit.short_id})")
is_expected.to have_body_text(commit.short_id)
end
end
@@ -712,7 +752,7 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do
- is_expected.to have_subject "Re: #{project.name} | #{commit.title.strip} (#{commit.short_id})"
+ is_expected.to have_subject "Re: #{project.name} | #{commit.title} (#{commit.short_id})"
end
it 'contains a link to the commit' do
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
index 9f41534441b..57ee2adaaff 100644
--- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb')
describe MigrateGcpClustersToNewClustersArchitectures, :migration do
- let(:project) { create(:project) }
+ let(:projects) { table(:projects) }
+ let(:project) { projects.create }
let(:user) { create(:user) }
- let(:service) { create(:kubernetes_service, project: project) }
+ let(:service) { create(:kubernetes_service, project_id: project.id) }
context 'when cluster is being created' do
let(:project_id) { project.id }
@@ -56,8 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.provider_type).to eq('gcp')
expect(cluster.platform_type).to eq('kubernetes')
- expect(cluster.project).to eq(project)
- expect(project.cluster).to eq(cluster)
+ expect(cluster.project_ids).to include(project.id)
expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status)
@@ -133,8 +133,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.provider_type).to eq('gcp')
expect(cluster.platform_type).to eq('kubernetes')
- expect(cluster.project).to eq(project)
- expect(project.cluster).to eq(cluster)
+ expect(cluster.project_ids).to include(project.id)
expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status)
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index 81366d15b34..92eb1d9ce86 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -16,20 +16,22 @@ describe MigrateOldArtifacts do
end
context 'with migratable data' do
- let(:project1) { create(:project, ci_id: 2) }
- let(:project2) { create(:project, ci_id: 3) }
- let(:project3) { create(:project) }
+ set(:project1) { create(:project, ci_id: 2) }
+ set(:project2) { create(:project, ci_id: 3) }
+ set(:project3) { create(:project) }
- let(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
- let(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
- let(:pipeline3) { create(:ci_empty_pipeline, project: project3) }
+ set(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
+ set(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+ set(:pipeline3) { create(:ci_empty_pipeline, project: project3) }
let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) }
let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) }
- let!(:build2) { create(:ci_build, :artifacts, pipeline: pipeline2) }
- let!(:build3) { create(:ci_build, :artifacts, pipeline: pipeline3) }
+ let!(:build2) { create(:ci_build, pipeline: pipeline2) }
+ let!(:build3) { create(:ci_build, pipeline: pipeline3) }
before do
+ setup_builds(build2, build3)
+
store_artifacts_in_legacy_path(build_with_legacy_artifacts)
end
@@ -38,7 +40,7 @@ describe MigrateOldArtifacts do
end
it "legacy artifacts are set" do
- expect(build_with_legacy_artifacts.artifacts_file_identifier).not_to be_nil
+ expect(build_with_legacy_artifacts.legacy_artifacts_file_identifier).not_to be_nil
end
describe '#min_id' do
@@ -113,5 +115,24 @@ describe MigrateOldArtifacts do
build.project.ci_id.to_s,
build.id.to_s)
end
+
+ def new_legacy_path(build)
+ File.join(directory,
+ build.created_at.utc.strftime('%Y_%m'),
+ build.project_id.to_s,
+ build.id.to_s)
+ end
+
+ def setup_builds(*builds)
+ builds.each do |build|
+ FileUtils.mkdir_p(new_legacy_path(build))
+
+ build.update_columns(
+ artifacts_file: 'ci_build_artifacts.zip',
+ artifacts_metadata: 'ci_build_artifacts_metadata.gz')
+
+ build.reload
+ end
+ end
end
end
diff --git a/spec/migrations/remove_assignee_id_from_issue_spec.rb b/spec/migrations/remove_assignee_id_from_issue_spec.rb
new file mode 100644
index 00000000000..2c6f992d3ae
--- /dev/null
+++ b/spec/migrations/remove_assignee_id_from_issue_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170523073948_remove_assignee_id_from_issue.rb')
+
+describe RemoveAssigneeIdFromIssue, :migration do
+ let(:issues) { table(:issues) }
+ let(:issue_assignees) { table(:issue_assignees) }
+ let(:users) { table(:users) }
+
+ let!(:user_1) { users.create(email: 'email1@example.com') }
+ let!(:user_2) { users.create(email: 'email2@example.com') }
+ let!(:user_3) { users.create(email: 'email3@example.com') }
+
+ def create_issue(assignees:)
+ issues.create.tap do |issue|
+ assignees.each do |assignee|
+ issue_assignees.create(issue_id: issue.id, user_id: assignee.id)
+ end
+ end
+ end
+
+ let!(:issue_single_assignee) { create_issue(assignees: [user_1]) }
+ let!(:issue_no_assignee) { create_issue(assignees: []) }
+ let!(:issue_multiple_assignees) { create_issue(assignees: [user_2, user_3]) }
+
+ describe '#down' do
+ it 'sets the assignee_id to a random matching assignee from the assignees table' do
+ migrate!
+ disable_migrations_output { described_class.new.down }
+
+ expect(issue_single_assignee.reload.assignee_id).to eq(user_1.id)
+ expect(issue_no_assignee.reload.assignee_id).to be_nil
+ expect(issue_multiple_assignees.reload.assignee_id).to eq(user_2.id).or(user_3.id)
+
+ disable_migrations_output { described_class.new.up }
+ end
+ end
+end
diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..7fe7a140e2f
--- /dev/null
+++ b/spec/migrations/track_untracked_uploads_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads')
+
+describe TrackUntrackedUploads, :migration, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ matcher :be_scheduled_migration do
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ it 'correctly schedules the follow-up background migration' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 49f44525b29..56b5d616284 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -5,9 +5,6 @@ describe Appearance do
it { is_expected.to be_valid }
- it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_presence_of(:description) }
-
it { is_expected.to have_many(:uploads).dependent(:destroy) }
describe '.current', :use_clean_rails_memory_store_caching do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 0b7e16cc33c..ef480e7a80a 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -115,9 +115,8 @@ describe ApplicationSetting do
end
context 'circuitbreaker settings' do
- [:circuitbreaker_backoff_threshold,
- :circuitbreaker_failure_count_threshold,
- :circuitbreaker_failure_wait_time,
+ [:circuitbreaker_failure_count_threshold,
+ :circuitbreaker_check_interval,
:circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout].each do |field|
it "Validates #{field} as number" do
@@ -126,16 +125,6 @@ describe ApplicationSetting do
.is_greater_than_or_equal_to(0)
end
end
-
- it 'requires the `backoff_threshold` to be lower than the `failure_count_threshold`' do
- setting.circuitbreaker_failure_count_threshold = 10
- setting.circuitbreaker_backoff_threshold = 15
- failure_message = "The circuitbreaker backoff threshold should be lower "\
- "than the failure count threshold"
-
- expect(setting).not_to be_valid
- expect(setting.errors[:circuitbreaker_backoff_threshold]).to include(failure_message)
- end
end
context 'repository storages' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 1795ee8e9a4..871e8b47650 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -23,6 +23,8 @@ describe Ci::Build do
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
+ it { is_expected.to be_a(ArtifactMigratable) }
+
describe 'callbacks' do
context 'when running after_create callback' do
it 'triggers asynchronous build hooks worker' do
@@ -132,32 +134,43 @@ describe Ci::Build do
describe '#artifacts?' do
subject { build.artifacts? }
- context 'artifacts archive does not exist' do
- before do
- build.update_attributes(artifacts_file: nil)
+ context 'when new artifacts are used' do
+ context 'artifacts archive does not exist' do
+ let(:build) { create(:ci_build) }
+
+ it { is_expected.to be_falsy }
end
- it { is_expected.to be_falsy }
- end
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :artifacts) }
- context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_truthy }
- context 'is expired' do
- before do
- build.update(artifacts_expire_at: Time.now - 7.days)
+ context 'is expired' do
+ let(:build) { create(:ci_build, :artifacts, :expired) }
+
+ it { is_expected.to be_falsy }
end
+ end
+ end
+
+ context 'when legacy artifacts are used' do
+ context 'artifacts archive does not exist' do
+ let(:build) { create(:ci_build) }
it { is_expected.to be_falsy }
end
- context 'is not expired' do
- before do
- build.update(artifacts_expire_at: Time.now + 7.days)
- end
+ context 'artifacts archive exists' do
+ let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy }
+
+ context 'is expired' do
+ let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
+
+ it { is_expected.to be_falsy }
+ end
end
end
end
@@ -612,71 +625,144 @@ describe Ci::Build do
describe '#erasable?' do
subject { build.erasable? }
+
it { is_expected.to eq false }
end
end
context 'build is erasable' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ context 'new artifacts' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
- describe '#erase' do
- before do
- build.erase(erased_by: user)
- end
+ describe '#erase' do
+ before do
+ build.erase(erased_by: user)
+ end
- context 'erased by user' do
- let!(:user) { create(:user, username: 'eraser') }
+ context 'erased by user' do
+ let!(:user) { create(:user, username: 'eraser') }
- include_examples 'erasable'
+ include_examples 'erasable'
- it 'records user who erased a build' do
- expect(build.erased_by).to eq user
+ it 'records user who erased a build' do
+ expect(build.erased_by).to eq user
+ end
end
- end
- context 'erased by system' do
- let(:user) { nil }
+ context 'erased by system' do
+ let(:user) { nil }
- include_examples 'erasable'
+ include_examples 'erasable'
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
+ it 'does not set user who erased a build' do
+ expect(build.erased_by).to be_nil
+ end
end
end
- end
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to be_truthy }
- end
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to be_truthy }
+ end
- describe '#erased?' do
- let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
- subject { build.erased? }
+ describe '#erased?' do
+ let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
+ subject { build.erased? }
- context 'job has not been erased' do
- it { is_expected.to be_falsey }
+ context 'job has not been erased' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'job has been erased' do
+ before do
+ build.erase
+ end
+
+ it { is_expected.to be_truthy }
+ end
end
- context 'job has been erased' do
+ context 'metadata and build trace are not available' do
+ let!(:build) { create(:ci_build, :success, :artifacts) }
+
before do
- build.erase
+ build.remove_artifacts_metadata!
end
- it { is_expected.to be_truthy }
+ describe '#erase' do
+ it 'does not raise error' do
+ expect { build.erase }.not_to raise_error
+ end
+ end
end
end
+ end
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :artifacts) }
+ context 'old artifacts' do
+ context 'build is erasable' do
+ context 'new artifacts' do
+ let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) }
- before do
- build.remove_artifacts_metadata!
- end
+ describe '#erase' do
+ before do
+ build.erase(erased_by: user)
+ end
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
+ context 'erased by user' do
+ let!(:user) { create(:user, username: 'eraser') }
+
+ include_examples 'erasable'
+
+ it 'records user who erased a build' do
+ expect(build.erased_by).to eq user
+ end
+ end
+
+ context 'erased by system' do
+ let(:user) { nil }
+
+ include_examples 'erasable'
+
+ it 'does not set user who erased a build' do
+ expect(build.erased_by).to be_nil
+ end
+ end
+ end
+
+ describe '#erasable?' do
+ subject { build.erasable? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#erased?' do
+ let!(:build) { create(:ci_build, :trace, :success, :legacy_artifacts) }
+ subject { build.erased? }
+
+ context 'job has not been erased' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'job has been erased' do
+ before do
+ build.erase
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'metadata and build trace are not available' do
+ let!(:build) { create(:ci_build, :success, :legacy_artifacts) }
+
+ before do
+ build.remove_artifacts_metadata!
+ end
+
+ describe '#erase' do
+ it 'does not raise error' do
+ expect { build.erase }.not_to raise_error
+ end
+ end
end
end
end
@@ -912,11 +998,23 @@ describe Ci::Build do
describe '#keep_artifacts!' do
let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) }
+ subject { build.keep_artifacts! }
+
it 'to reset expire_at' do
- build.keep_artifacts!
+ subject
expect(build.artifacts_expire_at).to be_nil
end
+
+ context 'when having artifacts files' do
+ let!(:artifact) { create(:ci_job_artifact, job: build, expire_in: '7 days') }
+
+ it 'to reset dependent objects' do
+ subject
+
+ expect(artifact.reload.expire_at).to be_nil
+ end
+ end
end
describe '#merge_request' do
@@ -1241,10 +1339,10 @@ describe Ci::Build do
context 'when config does not have a questioned job' do
let(:config) do
YAML.dump({
- test_other: {
- script: 'Hello World'
- }
- })
+ test_other: {
+ script: 'Hello World'
+ }
+ })
end
it { is_expected.to eq('on_success') }
@@ -1253,11 +1351,11 @@ describe Ci::Build do
context 'when config has `when`' do
let(:config) do
YAML.dump({
- test: {
- script: 'Hello World',
- when: 'always'
- }
- })
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
end
it { is_expected.to eq('always') }
@@ -1338,10 +1436,10 @@ describe Ci::Build do
let!(:environment) do
create(:environment,
- project: build.project,
- name: 'production',
- slug: 'prod-slug',
- external_url: '')
+ project: build.project,
+ name: 'production',
+ slug: 'prod-slug',
+ external_url: '')
end
before do
@@ -1565,8 +1663,8 @@ describe Ci::Build do
let!(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable,
- key: 'SCHEDULE_VARIABLE_KEY',
- pipeline_schedule: pipeline_schedule)
+ key: 'SCHEDULE_VARIABLE_KEY',
+ pipeline_schedule: pipeline_schedule)
end
before do
@@ -1708,8 +1806,8 @@ describe Ci::Build do
allow_any_instance_of(Project)
.to receive(:secret_variables_for)
.with(ref: 'master', environment: nil) do
- [create(:ci_variable, key: 'secret', value: 'value')]
- end
+ [create(:ci_variable, key: 'secret', value: 'value')]
+ end
allow_any_instance_of(Ci::Pipeline)
.to receive(:predefined_variables) { [pipeline_pre_var] }
@@ -1760,6 +1858,93 @@ describe Ci::Build do
end
end
+ describe 'state transition: any => [:running]' do
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.not_to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+ end
+
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+ end
+
+ let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) }
+
+ context 'when validates for dependencies is enabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ context 'when "dependencies" keyword is not defined' do
+ let(:options) { {} }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when "dependencies" keyword is empty' do
+ let(:options) { { dependencies: [] } }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when "dependencies" keyword is specified' do
+ let(:options) { { dependencies: ['test'] } }
+
+ it_behaves_like 'validation is active'
+ end
+ end
+
+ context 'when validates for dependencies is disabled' do
+ let(:options) { { dependencies: ['test'] } }
+
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: true)
+ end
+
+ it_behaves_like 'validation is not active'
+ end
+ end
+
describe 'state transition when build fails' do
let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user) }
@@ -1813,4 +1998,77 @@ describe Ci::Build do
end
end
end
+
+ describe '.matches_tag_ids' do
+ set(:build) { create(:ci_build, project: project, user: user) }
+ let(:tag_ids) { ::ActsAsTaggableOn::Tag.named_any(tag_list).ids }
+
+ subject { described_class.where(id: build).matches_tag_ids(tag_ids) }
+
+ before do
+ build.update(tag_list: build_tag_list)
+ end
+
+ context 'when have different tags' do
+ let(:build_tag_list) { %w(A B) }
+ let(:tag_list) { %w(C D) }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+
+ context 'when have a subset of tags' do
+ let(:build_tag_list) { %w(A B) }
+ let(:tag_list) { %w(A B C D) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when build does not have tags' do
+ let(:build_tag_list) { [] }
+ let(:tag_list) { %w(C D) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when does not have a subset of tags' do
+ let(:build_tag_list) { %w(A B C) }
+ let(:tag_list) { %w(C D) }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+ end
+
+ describe '.matches_tags' do
+ set(:build) { create(:ci_build, project: project, user: user) }
+
+ subject { described_class.where(id: build).with_any_tags }
+
+ before do
+ build.update(tag_list: tag_list)
+ end
+
+ context 'when does have tags' do
+ let(:tag_list) { %w(A B) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when does not have tags' do
+ let(:tag_list) { [] }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
new file mode 100644
index 00000000000..0e18a326c68
--- /dev/null
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Ci::JobArtifact do
+ set(:artifact) { create(:ci_job_artifact, :archive) }
+
+ describe "Associations" do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:job) }
+ end
+
+ it { is_expected.to respond_to(:file) }
+ it { is_expected.to respond_to(:created_at) }
+ it { is_expected.to respond_to(:updated_at) }
+
+ describe '#set_size' do
+ it 'sets the size' do
+ expect(artifact.size).to eq(106365)
+ end
+ end
+
+ describe '#file' do
+ subject { artifact.file }
+
+ context 'the uploader api' do
+ it { is_expected.to respond_to(:store_dir) }
+ it { is_expected.to respond_to(:cache_dir) }
+ it { is_expected.to respond_to(:work_dir) }
+ end
+ end
+
+ describe '#expire_in' do
+ subject { artifact.expire_in }
+
+ it { is_expected.to be_nil }
+
+ context 'when expire_at is specified' do
+ let(:expire_at) { Time.now + 7.days }
+
+ before do
+ artifact.expire_at = expire_at
+ end
+
+ it { is_expected.to be_within(5).of(expire_at - Time.now) }
+ end
+ end
+
+ describe '#expire_in=' do
+ subject { artifact.expire_in }
+
+ it 'when assigning valid duration' do
+ artifact.expire_in = '7 days'
+
+ is_expected.to be_within(10).of(7.days.to_i)
+ end
+
+ it 'when assigning invalid duration' do
+ expect { artifact.expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError)
+
+ is_expected.to be_nil
+ end
+
+ it 'when resetting value' do
+ artifact.expire_in = nil
+
+ is_expected.to be_nil
+ end
+
+ it 'when setting to 0' do
+ artifact.expire_in = '0'
+
+ is_expected.to be_nil
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3a19a0753e2..bb89e093890 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -557,10 +557,23 @@ describe Ci::Pipeline, :mailer do
describe '#has_kubernetes_active?' do
context 'when kubernetes is active' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns true' do
+ expect(pipeline).to have_kubernetes_active
+ end
+ end
- it 'returns true' do
- expect(pipeline).to have_kubernetes_active
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -855,62 +868,59 @@ describe Ci::Pipeline, :mailer do
end
describe '#set_config_source' do
- context 'on object initialisation' do
- context 'when pipelines does not contain needed data' do
- let(:pipeline) do
- Ci::Pipeline.new
- end
+ context 'when pipelines does not contain needed data' do
+ it 'defines source to be unknown' do
+ pipeline.set_config_source
- it 'defines source to be unknown' do
- expect(pipeline).to be_unknown_source
- end
+ expect(pipeline).to be_unknown_source
end
+ end
- context 'when pipeline contains all needed data' do
- let(:pipeline) do
- Ci::Pipeline.new(
- project: project,
- sha: '1234',
- ref: 'master',
- source: :push)
+ context 'when pipeline contains all needed data' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: '1234',
+ ref: 'master',
+ source: :push)
+ end
+
+ context 'when the repository has a config file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
end
- context 'when the repository has a config file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml_for)
- .and_return('config')
- end
+ it 'defines source to be from repository' do
+ pipeline.set_config_source
- it 'defines source to be from repository' do
- expect(pipeline).to be_repository_source
- end
+ expect(pipeline).to be_repository_source
+ end
- context 'when loading an object' do
- let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) }
+ context 'when loading an object' do
+ let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) }
- it 'does not redefine the source' do
- # force to overwrite the source
- pipeline.unknown_source!
+ it 'does not redefine the source' do
+ # force to overwrite the source
+ pipeline.unknown_source!
- expect(new_pipeline).to be_unknown_source
- end
+ expect(new_pipeline).to be_unknown_source
end
end
+ end
- context 'when the repository does not have a config file' do
- let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
+ context 'when the repository does not have a config file' do
+ let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
- context 'auto devops enabled' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- allow(project).to receive(:ci_config_path) { 'custom' }
- end
+ context 'auto devops enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ allow(project).to receive(:ci_config_path) { 'custom' }
+ end
- it 'defines source to be auto devops' do
- subject
+ it 'defines source to be auto devops' do
+ pipeline.set_config_source
- expect(pipeline).to be_auto_devops_source
- end
+ expect(pipeline).to be_auto_devops_source
end
end
end
@@ -1234,7 +1244,7 @@ describe Ci::Pipeline, :mailer do
describe '#execute_hooks' do
let!(:build_a) { create_build('a', 0) }
- let!(:build_b) { create_build('b', 1) }
+ let!(:build_b) { create_build('b', 0) }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
@@ -1290,6 +1300,8 @@ describe Ci::Pipeline, :mailer do
end
context 'when stage one failed' do
+ let!(:build_b) { create_build('b', 1) }
+
before do
build_a.drop
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index b91a5e7a272..2683d21ddbe 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -9,7 +9,6 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
it { is_expected.to delegate_method(:on_creation?).to(:provider) }
- it { is_expected.to delegate_method(:update_kubernetes_integration!).to(:platform) }
it { is_expected.to respond_to :project }
describe '.enabled' do
@@ -199,4 +198,26 @@ describe Clusters::Cluster do
end
end
end
+
+ describe '#created?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.created? }
+
+ context 'when status_name is :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:created)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when status_name is not :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:creating)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index ed76be703a5..53a4e545ff6 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -5,6 +5,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
include ReactiveCachingHelpers
it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to be_kind_of(Gitlab::Kubernetes) }
+ it { is_expected.to be_kind_of(ReactiveCaching) }
it { is_expected.to respond_to :ca_pem }
describe 'before_validation' do
@@ -90,99 +92,175 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
end
- describe 'after_save from Clusters::Cluster' do
- context 'when platform_kubernetes is being cerated' do
- let(:enabled) { true }
- let(:project) { create(:project) }
- let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, enabled: enabled, projects: [project]) }
- let(:platform) { build(:cluster_platform_kubernetes, :configured) }
- let(:provider) { build(:cluster_provider_gcp) }
- let(:kubernetes_service) { project.kubernetes_service }
+ describe '#actual_namespace' do
+ subject { kubernetes.actual_namespace }
- it 'updates KubernetesService' do
- cluster.save!
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
- expect(kubernetes_service.active).to eq(enabled)
- expect(kubernetes_service.api_url).to eq(platform.api_url)
- expect(kubernetes_service.namespace).to eq(platform.namespace)
- expect(kubernetes_service.ca_pem).to eq(platform.ca_cert)
- end
+ context 'when namespace is present' do
+ let(:namespace) { 'namespace-123' }
+
+ it { is_expected.to eq(namespace) }
end
- context 'when platform_kubernetes has been created' do
- let(:enabled) { false }
- let!(:project) { create(:project) }
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:platform) { cluster.platform }
- let(:kubernetes_service) { project.kubernetes_service }
+ context 'when namespace is not present' do
+ let(:namespace) { nil }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+ end
- it 'updates KubernetesService' do
- cluster.update(enabled: enabled)
+ describe '#default_namespace' do
+ subject { kubernetes.send(:default_namespace) }
- expect(kubernetes_service.active).to eq(enabled)
+ let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+
+ context 'when cluster belongs to a project' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:project) { cluster.project }
+
+ it { is_expected.to eq("#{project.path}-#{project.id}") }
+ end
+
+ context 'when cluster belongs to nothing' do
+ let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#predefined_variables' do
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let(:kubernetes) { create(:cluster_platform_kubernetes, api_url: api_url, ca_cert: ca_pem, token: token) }
+ let(:api_url) { 'https://kube.domain.com' }
+ let(:ca_pem) { 'CA PEM DATA' }
+ let(:token) { 'token' }
+
+ let(:kubeconfig) do
+ config_file = expand_fixture_path('config/kubeconfig.yml')
+ config = YAML.load(File.read(config_file))
+ config.dig('users', 0, 'user')['token'] = token
+ config.dig('contexts', 0, 'context')['namespace'] = namespace
+ config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
+ Base64.strict_encode64(ca_pem)
+
+ YAML.dump(config)
+ end
+
+ shared_examples 'setting variables' do
+ it 'sets the variables' do
+ expect(kubernetes.predefined_variables).to include(
+ { key: 'KUBE_URL', value: api_url, public: true },
+ { key: 'KUBE_TOKEN', value: token, public: false },
+ { key: 'KUBE_NAMESPACE', value: namespace, public: true },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
+ { key: 'KUBE_CA_PEM', value: ca_pem, public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: ca_pem, public: true, file: true }
+ )
end
end
- context 'when kubernetes_service has been configured without cluster integration' do
- let!(:project) { create(:project) }
- let(:cluster) { build(:cluster, provider_type: :gcp, platform_type: :kubernetes, platform_kubernetes: platform, provider_gcp: provider, projects: [project]) }
- let(:platform) { build(:cluster_platform_kubernetes, :configured, api_url: 'https://111.111.111.111') }
- let(:provider) { build(:cluster_provider_gcp) }
+ context 'namespace is provided' do
+ let(:namespace) { 'my-project' }
before do
- create(:kubernetes_service, project: project)
+ kubernetes.namespace = namespace
end
- it 'raises an error' do
- expect { cluster.save! }.to raise_error('Kubernetes service already configured')
+ it_behaves_like 'setting variables'
+ end
+
+ context 'no namespace provided' do
+ let(:namespace) { kubernetes.actual_namespace }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = kubernetes.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
end
end
end
- describe '#actual_namespace' do
- subject { kubernetes.actual_namespace }
+ describe '#terminals' do
+ subject { service.terminals(environment) }
- let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
+ let!(:cluster) { create(:cluster, :project, platform_kubernetes: service) }
let(:project) { cluster.project }
- let(:kubernetes) { create(:cluster_platform_kubernetes, :configured, namespace: namespace) }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
- context 'when namespace is present' do
- let(:namespace) { 'namespace-123' }
+ context 'with invalid pods' do
+ it 'returns no terminals' do
+ stub_reactive_cache(service, pods: [{ "bad" => "pod" }])
- it { is_expected.to eq(namespace) }
+ is_expected.to be_empty
+ end
end
- context 'when namespace is not present' do
- let(:namespace) { nil }
+ context 'with valid pods' do
+ let(:pod) { kube_pod(app: environment.slug) }
+ let(:terminals) { kube_terminals(service, pod) }
- it { is_expected.to eq("#{project.path}-#{project.id}") }
+ before do
+ stub_reactive_cache(
+ service,
+ pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
+ )
+ end
+
+ it 'returns terminals' do
+ is_expected.to eq(terminals + terminals)
+ end
+
+ it 'uses max session time from settings' do
+ stub_application_setting(terminal_max_session_time: 600)
+
+ times = subject.map { |terminal| terminal[:max_session_time] }
+ expect(times).to eq [600, 600, 600, 600]
+ end
end
end
- describe '.namespace_for_project' do
- subject { described_class.namespace_for_project(project) }
+ describe '#calculate_reactive_cache' do
+ subject { service.calculate_reactive_cache }
- let(:project) { create(:project) }
+ let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) }
+ let(:service) { create(:cluster_platform_kubernetes, :configured) }
+ let(:enabled) { true }
- it { is_expected.to eq("#{project.path}-#{project.id}") }
- end
+ context 'when cluster is disabled' do
+ let(:enabled) { false }
- describe '#default_namespace' do
- subject { kubernetes.default_namespace }
+ it { is_expected.to be_nil }
+ end
- let(:kubernetes) { create(:cluster_platform_kubernetes, :configured) }
+ context 'when kubernetes responds with valid pods' do
+ before do
+ stub_kubeclient_pods
+ end
- context 'when cluster belongs to a project' do
- let!(:cluster) { create(:cluster, :project, platform_kubernetes: kubernetes) }
- let(:project) { cluster.project }
+ it { is_expected.to eq(pods: [kube_pod]) }
+ end
- it { is_expected.to eq("#{project.path}-#{project.id}") }
+ context 'when kubernetes responds with 500s' do
+ before do
+ stub_kubeclient_pods(status: 500)
+ end
+
+ it { expect { subject }.to raise_error(KubeException) }
end
- context 'when cluster belongs to nothing' do
- let!(:cluster) { create(:cluster, platform_kubernetes: kubernetes) }
+ context 'when kubernetes responds with 404s' do
+ before do
+ stub_kubeclient_pods(status: 404)
+ end
- it { is_expected.to be_nil }
+ it { is_expected.to eq(pods: []) }
end
end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 129dfa07f15..3c7f578975b 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -102,6 +102,26 @@ describe CacheMarkdownField do
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
+ context 'when a markdown field is set repeatedly to an empty string' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.foo = ''
+ thing.save
+ thing.foo = ''
+ thing.save
+ end
+ end
+
+ context 'when a markdown field is set repeatedly to a string which renders as empty html' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.foo = '[//]: # (This is also a comment.)'
+ thing.save
+ thing.foo = '[//]: # (This is also a comment.)'
+ thing.save
+ end
+ end
+
context 'a non-markdown field changed' do
before do
thing.bar = 'OK'
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index a53b59c4e08..9df26f06a11 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -171,7 +171,7 @@ describe Issuable do
it "returns false when record has been updated" do
allow(issue).to receive(:today?).and_return(true)
- issue.touch
+ issue.update_attribute(:updated_at, 1.hour.ago)
expect(issue.new?).to be_falsey
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 8389d5c5430..4d0b3245a13 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -9,13 +9,14 @@ describe DiffNote do
let(:path) { "files/ruby/popen.rb" }
+ let(:diff_refs) { merge_request.diff_refs }
let!(:position) do
Gitlab::Diff::Position.new(
old_path: path,
new_path: path,
old_line: nil,
new_line: 14,
- diff_refs: merge_request.diff_refs
+ diff_refs: diff_refs
)
end
@@ -25,7 +26,7 @@ describe DiffNote do
new_path: path,
old_line: 16,
new_line: 22,
- diff_refs: merge_request.diff_refs
+ diff_refs: diff_refs
)
end
@@ -158,25 +159,21 @@ describe DiffNote do
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
- let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
+ let(:diff_refs) { commit.diff_refs }
- it "doesn't update the position" do
- diff_note
+ subject { create(:diff_note_on_commit, project: project, position: position, commit_id: commit.id) }
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).to eq(position)
+ it "doesn't update the position" do
+ is_expected.to have_attributes(original_position: position,
+ position: position)
end
end
context "when noteable is a merge request" do
- let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
-
context "when the note is active" do
it "doesn't update the position" do
- diff_note
-
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).to eq(position)
+ expect(subject.original_position).to eq(position)
+ expect(subject.position).to eq(position)
end
end
@@ -186,10 +183,8 @@ describe DiffNote do
end
it "updates the position" do
- diff_note
-
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).not_to eq(position)
+ expect(subject.original_position).to eq(position)
+ expect(subject.position).not_to eq(position)
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 1ce1d595c60..6f24a039998 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -327,15 +327,28 @@ describe Environment do
context 'when the enviroment is available' do
context 'with a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
- context 'and a deployment' do
- let!(:deployment) { create(:deployment, environment: environment) }
- it { is_expected.to be_truthy }
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
end
- context 'but no deployments' do
- it { is_expected.to be_falsy }
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
@@ -356,7 +369,6 @@ describe Environment do
end
describe '#terminals' do
- let(:project) { create(:kubernetes_project) }
subject { environment.terminals }
context 'when the environment has terminals' do
@@ -364,12 +376,27 @@ describe Environment do
allow(environment).to receive(:has_terminals?).and_return(true)
end
- it 'returns the terminals from the deployment service' do
- expect(project.deployment_service)
- .to receive(:terminals).with(environment)
- .and_return(:fake_terminals)
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns the terminals from the deployment service' do
+ expect(project.deployment_platform)
+ .to receive(:terminals).with(environment)
+ .and_return(:fake_terminals)
+
+ is_expected.to eq(:fake_terminals)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- is_expected.to eq(:fake_terminals)
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 5f901262598..0ea287d007a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -765,4 +765,8 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue])
end
end
+
+ it_behaves_like 'throttled touch' do
+ subject { create(:issue, updated_at: 1.hour.ago) }
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 728028746d8..bb63abd167b 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -124,6 +124,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
project.repository.rm_branch(subject.author, subject.target_branch)
+ subject.clear_memoized_shas
end
it 'returns nil' do
@@ -600,30 +601,30 @@ describe MergeRequest do
end
describe '#can_remove_source_branch?' do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ set(:user) { create(:user) }
+ set(:merge_request) { create(:merge_request, :simple) }
- before do
- subject.source_project.team << [user, :master]
+ subject { merge_request }
- subject.source_branch = "feature"
- subject.target_branch = "master"
- subject.save!
+ before do
+ subject.source_project.add_master(user)
end
it "can't be removed when its a protected branch" do
allow(ProtectedBranch).to receive(:protected?).and_return(true)
+
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "can't remove a root ref" do
- subject.source_branch = "master"
- subject.target_branch = "feature"
+ subject.update(source_branch: 'master', target_branch: 'feature')
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
it "is unable to remove the source branch for a project the user cannot push to" do
+ user2 = create(:user)
+
expect(subject.can_remove_source_branch?(user2)).to be_falsey
end
@@ -634,6 +635,7 @@ describe MergeRequest do
end
it "cannot be removed if the last commit is not also the head of the source branch" do
+ subject.clear_memoized_shas
subject.source_branch = "lfs"
expect(subject.can_remove_source_branch?(user)).to be_falsey
@@ -733,7 +735,7 @@ describe MergeRequest do
before do
project.repository.raw_repository.delete_branch(subject.target_branch)
- subject.reload
+ subject.clear_memoized_shas
end
it 'does not crash' do
@@ -827,20 +829,47 @@ describe MergeRequest do
end
end
- describe '#head_pipeline' do
- describe 'when the source project exists' do
- it 'returns the latest pipeline' do
- pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc", head_pipeline_of: subject)
+ context 'head pipeline' do
+ before do
+ allow(subject).to receive(:diff_head_sha).and_return('lastsha')
+ end
- expect(subject.head_pipeline).to eq(pipeline)
+ describe '#head_pipeline' do
+ it 'returns nil for MR without head_pipeline_id' do
+ subject.update_attribute(:head_pipeline_id, nil)
+
+ expect(subject.head_pipeline).to be_nil
+ end
+
+ context 'when the source project does not exist' do
+ it 'returns nil' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.head_pipeline).to be_nil
+ end
end
end
- describe 'when the source project does not exist' do
- it 'returns nil' do
+ describe '#actual_head_pipeline' do
+ it 'returns nil for MR with old pipeline' do
+ pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
+ subject.update_attribute(:head_pipeline_id, pipeline.id)
+
+ expect(subject.actual_head_pipeline).to be_nil
+ end
+
+ it 'returns the pipeline for MR with recent pipeline' do
+ pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
+ subject.update_attribute(:head_pipeline_id, pipeline.id)
+
+ expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
+ expect(subject.actual_head_pipeline).to eq(pipeline)
+ end
+
+ it 'returns nil when source project does not exist' do
allow(subject).to receive(:source_project).and_return(nil)
- expect(subject.head_pipeline).to be_nil
+ expect(subject.actual_head_pipeline).to be_nil
end
end
end
@@ -940,7 +969,7 @@ describe MergeRequest do
end
shared_examples 'returning all SHA' do
- it 'returns all SHA from all merge_request_diffs' do
+ it 'returns all SHAs from all merge_request_diffs' do
expect(subject.merge_request_diffs.size).to eq(2)
expect(subject.all_commit_shas).to match_array(all_commit_shas)
end
@@ -1179,7 +1208,7 @@ describe MergeRequest do
context 'when it is only allowed to merge when build is green' do
context 'and a failed pipeline is associated' do
before do
- pipeline.update(status: 'failed')
+ pipeline.update(status: 'failed', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1188,7 +1217,7 @@ describe MergeRequest do
context 'and a successful pipeline is associated' do
before do
- pipeline.update(status: 'success')
+ pipeline.update(status: 'success', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1197,7 +1226,7 @@ describe MergeRequest do
context 'and a skipped pipeline is associated' do
before do
- pipeline.update(status: 'skipped')
+ pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1377,6 +1406,16 @@ describe MergeRequest do
subject.reload_diff
end
+
+ context 'when using the after_update hook to update' do
+ context 'when the branches are updated' do
+ it 'uses the new heads to generate the diff' do
+ expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) }
+ .to change { subject.merge_request_diff.start_commit_sha }
+ .and change { subject.merge_request_diff.head_commit_sha }
+ end
+ end
+ end
end
describe '#update_diff_discussion_positions' do
@@ -1441,6 +1480,7 @@ describe MergeRequest do
context 'when the target branch does not exist' do
before do
subject.project.repository.rm_branch(subject.author, subject.target_branch)
+ subject.clear_memoized_shas
end
it 'returns nil' do
@@ -1824,4 +1864,24 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
+
+ it_behaves_like 'throttled touch' do
+ subject { create(:merge_request, updated_at: 1.hour.ago) }
+ end
+
+ context 'state machine transitions' do
+ describe '#unlock_mr' do
+ subject { create(:merge_request, state: 'locked', merge_jid: 123) }
+
+ it 'updates merge request head pipeline and sets merge_jid to nil' do
+ pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha)
+
+ subject.unlock_mr
+
+ subject.reload
+ expect(subject.head_pipeline).to eq(pipeline)
+ expect(subject.merge_jid).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 90b768f595e..b7c6286fd83 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -531,7 +531,7 @@ describe Namespace do
end
end
- describe '#has_forks_of?' do
+ describe '#find_fork_of?' do
let(:project) { create(:project, :public) }
let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) }
@@ -550,5 +550,43 @@ describe Namespace do
expect(other_namespace.find_fork_of(project)).to eq(other_fork)
end
+
+ context 'with request store enabled', :request_store do
+ it 'only queries once' do
+ expect(project.fork_network).to receive(:find_forks_in).once.and_call_original
+
+ 2.times { namespace.find_fork_of(project) }
+ end
+ end
+ end
+
+ describe "#allowed_path_by_redirects" do
+ let(:namespace1) { create(:namespace, path: 'foo') }
+
+ context "when the path has been taken before" do
+ before do
+ namespace1.path = 'bar'
+ namespace1.save!
+ end
+
+ it 'should be invalid' do
+ namespace2 = build(:group, path: 'foo')
+ expect(namespace2).to be_invalid
+ end
+
+ 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')
+ 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
+ end
+ end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6e7e8c4c570..cefbf60b28c 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -5,7 +5,7 @@ describe Note do
describe 'associations' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:noteable).touch(true) }
+ 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) }
@@ -756,6 +756,28 @@ describe Note do
end
end
+ describe '#references' do
+ context 'when part of a discussion' do
+ it 'references all earlier notes in the discussion' do
+ first_note = create(:discussion_note_on_issue)
+ second_note = create(:discussion_note_on_issue, in_reply_to: first_note)
+ third_note = create(:discussion_note_on_issue, in_reply_to: second_note)
+ create(:discussion_note_on_issue, in_reply_to: third_note)
+
+ expect(third_note.references).to eq([first_note.noteable, first_note, second_note])
+ end
+ end
+
+ context 'when not part of a discussion' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'returns the noteable' do
+ expect(note.references).to eq([note.noteable])
+ end
+ end
+ end
+
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 01440b15674..2bb1c49b740 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe PersonalAccessToken do
+ subject { described_class }
+
describe '.build' do
let(:personal_access_token) { build(:personal_access_token) }
let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) }
@@ -45,6 +47,29 @@ describe PersonalAccessToken do
end
end
+ describe 'Redis storage' do
+ let(:user_id) { 123 }
+ let(:token) { 'abc000foo' }
+
+ before do
+ subject.redis_store!(user_id, token)
+ end
+
+ it 'returns stored data' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
+
+ context 'after deletion' do
+ before do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
+
+ it 'token is removed' do
+ expect(subject.redis_getdel(user_id)).to be_nil
+ end
+ end
+ end
+
context "validations" do
let(:personal_access_token) { build(:personal_access_token) }
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 1c629155e1e..f037ee77a94 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -4,8 +4,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
include KubernetesHelpers
include ReactiveCachingHelpers
- let(:project) { build_stubbed(:kubernetes_project) }
- let(:service) { project.kubernetes_service }
+ let(:project) { create(:kubernetes_project) }
+ let(:service) { project.deployment_platform }
describe 'Associations' do
it { is_expected.to belong_to :project }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 549c97a9afd..dd9e8498519 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -78,7 +78,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
- it { is_expected.to have_one(:cluster) }
+ it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do
@@ -138,6 +138,7 @@ describe Project do
it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
it { is_expected.to allow_value('').for(:ci_config_path) }
it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
+ it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
it { is_expected.to validate_presence_of(:creator) }
@@ -312,9 +313,7 @@ describe Project do
it { is_expected.to delegate_method(method).to(:team) }
end
- it { is_expected.to delegate_method(:empty_repo?).to(:repository) }
it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
- it { is_expected.to delegate_method(:count).to(:forks).with_prefix(true) }
it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
end
@@ -451,7 +450,7 @@ describe Project do
end
end
- describe "#new_issue_address" do
+ describe "#new_issuable_address" do
let(:project) { create(:project, path: "somewhere") }
let(:user) { create(:user) }
@@ -463,7 +462,13 @@ describe Project do
it 'returns the address to create a new issue' do
address = "p+#{project.full_path}+#{user.incoming_email_token}@gl.ab"
- expect(project.new_issue_address(user)).to eq(address)
+ expect(project.new_issuable_address(user, 'issue')).to eq(address)
+ end
+
+ it 'returns the address to create a new merge request' do
+ address = "p+#{project.full_path}+merge-request+#{user.incoming_email_token}@gl.ab"
+
+ expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
end
end
@@ -473,7 +478,11 @@ describe Project do
end
it 'returns nil' do
- expect(project.new_issue_address(user)).to be_nil
+ expect(project.new_issuable_address(user, 'issue')).to be_nil
+ end
+
+ it 'returns nil' do
+ expect(project.new_issuable_address(user, 'merge_request')).to be_nil
end
end
end
@@ -646,6 +655,24 @@ describe Project do
end
end
+ describe '#empty_repo?' do
+ context 'when the repo does not exist' do
+ let(:project) { build_stubbed(:project) }
+
+ it 'returns true' do
+ expect(project.empty_repo?).to be(true)
+ end
+ end
+
+ context 'when the repo exists' do
+ let(:project) { create(:project, :repository) }
+ let(:empty_project) { create(:project, :empty_repo) }
+
+ it { expect(empty_project.empty_repo?).to be(true) }
+ it { expect(project.empty_repo?).to be(false) }
+ end
+ end
+
describe '#external_issue_tracker' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
@@ -1548,8 +1575,8 @@ describe Project do
expect(project.ci_config_path).to eq('foo/.gitlab_ci.yml')
end
- it 'sets a string but removes all leading slashes and null characters' do
- project.update!(ci_config_path: "///f\0oo/\0/.gitlab_ci.yml")
+ it 'sets a string but removes all null characters' do
+ project.update!(ci_config_path: "f\0oo/\0/.gitlab_ci.yml")
expect(project.ci_config_path).to eq('foo//.gitlab_ci.yml')
end
@@ -1716,8 +1743,7 @@ describe Project do
expect(RepositoryForkWorker).to receive(:perform_async).with(
project.id,
forked_from_project.repository_storage_path,
- forked_from_project.disk_path,
- project.namespace.full_path).and_return(import_jid)
+ forked_from_project.disk_path).and_return(import_jid)
expect(project.add_import_job).to eq(import_jid)
end
@@ -1837,10 +1863,11 @@ describe Project do
project.change_head(project.default_branch)
end
- it 'creates the new reference with rugged' do
- expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+ it 'creates the new reference' do
+ expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD',
"refs/heads/#{project.default_branch}",
force: true)
+
project.change_head(project.default_branch)
end
@@ -2002,12 +2029,25 @@ describe Project do
end
context 'when project has a deployment service' do
- let(:project) { create(:kubernetes_project) }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ it 'returns variables from this service' do
+ expect(project.deployment_variables).to include(
+ { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false }
+ )
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- it 'returns variables from this service' do
- expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
- )
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
end
@@ -2459,7 +2499,7 @@ describe Project do
it 'returns the number of forks' do
project = build(:project)
- allow(project.forks).to receive(:count).and_return(1)
+ expect_any_instance_of(Projects::ForksCountService).to receive(:count).and_return(1)
expect(project.forks_count).to eq(1)
end
@@ -3083,4 +3123,23 @@ describe Project do
expect(project.wiki_repository_exists?).to eq(false)
end
end
+
+ describe '#deployment_platform' do
+ subject { project.deployment_platform }
+
+ let(:project) { create(:project) }
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let!(:kubernetes_service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to eq(kubernetes_service) }
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:platform_kubernetes) { cluster.platform_kubernetes }
+
+ it { is_expected.to eq(platform_kubernetes) }
+ end
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 59e20e84c2f..e78ed1df821 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -133,15 +133,29 @@ describe ProjectStatistics do
describe '#update_build_artifacts_size' do
let!(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:build1) { create(:ci_build, pipeline: pipeline, artifacts_size: 45.megabytes) }
- let!(:build2) { create(:ci_build, pipeline: pipeline, artifacts_size: 56.megabytes) }
- before do
- statistics.update_build_artifacts_size
+ context 'when new job artifacts are calculated' do
+ let(:ci_build) { create(:ci_build, pipeline: pipeline) }
+
+ before do
+ create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build)
+ end
+
+ it "stores the size of related build artifacts" do
+ statistics.update_build_artifacts_size
+
+ expect(statistics.build_artifacts_size).to be(106365)
+ end
end
- it "stores the size of related build artifacts" do
- expect(statistics.build_artifacts_size).to eq 101.megabytes
+ context 'when legacy artifacts are used' do
+ let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) }
+
+ it "stores the size of related build artifacts" do
+ statistics.update_build_artifacts_size
+
+ expect(statistics.build_artifacts_size).to eq(10.megabytes)
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 27f0a99b2fa..799d99c0369 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -29,7 +29,9 @@ describe Repository do
def expect_to_raise_storage_error
expect { yield }.to raise_error do |exception|
storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable]
- expect(exception.class).to be_in(storage_exceptions)
+ known_exception = storage_exceptions.select { |e| exception.is_a?(e) }
+
+ expect(known_exception).not_to be_nil
end
end
@@ -57,12 +59,18 @@ describe Repository do
end
describe 'tags_sorted_by' do
- context 'name' do
- subject { repository.tags_sorted_by('name').map(&:name) }
+ context 'name_desc' do
+ subject { repository.tags_sorted_by('name_desc').map(&:name) }
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
end
+ context 'name_asc' do
+ subject { repository.tags_sorted_by('name_asc').map(&:name) }
+
+ it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
+ end
+
context 'updated' do
let(:tag_a) { repository.find_tag('v1.0.0') }
let(:tag_b) { repository.find_tag('v1.1.0') }
@@ -583,7 +591,7 @@ describe Repository do
end
it 'properly handles query when repo is empty' do
- repository = create(:project).repository
+ repository = create(:project, :empty_repo).repository
results = repository.search_files_by_content('test', 'master')
expect(results).to match_array([])
@@ -619,7 +627,7 @@ describe Repository do
end
it 'properly handles query when repo is empty' do
- repository = create(:project).repository
+ repository = create(:project, :empty_repo).repository
results = repository.search_files_by_name('test', 'master')
@@ -634,9 +642,7 @@ describe Repository do
end
describe '#fetch_ref' do
- # Setting the var here, sidesteps the stub that makes gitaly raise an error
- # before the actual test call
- set(:broken_repository) { create(:project, :broken_storage).repository }
+ let(:broken_repository) { create(:project, :broken_storage).repository }
describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
@@ -1007,7 +1013,7 @@ describe Repository do
it 'runs without errors' do
# old_rev is an ancestor of new_rev
- expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
+ expect(repository.merge_base(old_rev, new_rev)).to eq(old_rev)
# old_rev is not a direct ancestor (parent) of new_rev
expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
@@ -1029,7 +1035,7 @@ describe Repository do
it 'raises an exception' do
# The 'master' branch is NOT an ancestor of new_rev.
- expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
+ expect(repository.merge_base(old_rev, new_rev)).not_to eq(old_rev)
# Updating 'master' to new_rev would lose the commits on 'master' that
# are not contained in new_rev. This should not be allowed.
@@ -1204,17 +1210,15 @@ describe Repository do
let(:empty_repository) { create(:project_empty_repo).repository }
it 'returns true for an empty repository' do
- expect(empty_repository.empty?).to eq(true)
+ expect(empty_repository).to be_empty
end
it 'returns false for a non-empty repository' do
- expect(repository.empty?).to eq(false)
+ expect(repository).not_to be_empty
end
it 'caches the output' do
- expect(repository.raw_repository).to receive(:empty?)
- .once
- .and_return(false)
+ expect(repository.raw_repository).to receive(:has_visible_content?).once
repository.empty?
repository.empty?
@@ -1372,78 +1376,98 @@ describe Repository do
end
describe '#revert' do
- let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
- let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
- let(:message) { 'revert message' }
-
- context 'when there is a conflict' do
- it 'raises an error' do
- expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ shared_examples 'reverting a commit' do
+ let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
+ let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:message) { 'revert message' }
+
+ context 'when there is a conflict' do
+ it 'raises an error' do
+ expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
end
- end
- context 'when commit was already reverted' do
- it 'raises an error' do
- repository.revert(user, update_image_commit, 'master', message)
+ context 'when commit was already reverted' do
+ it 'raises an error' do
+ repository.revert(user, update_image_commit, 'master', message)
- expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
end
- end
- context 'when commit can be reverted' do
- it 'reverts the changes' do
- expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
+ context 'when commit can be reverted' do
+ it 'reverts the changes' do
+ expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
+ end
end
- end
- context 'reverting a merge commit' do
- it 'reverts the changes' do
- merge_commit
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+ context 'reverting a merge commit' do
+ it 'reverts the changes' do
+ merge_commit
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
- repository.revert(user, merge_commit, 'master', message)
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
+ repository.revert(user, merge_commit, 'master', message)
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
+ end
end
end
+
+ context 'when Gitaly revert feature is enabled' do
+ it_behaves_like 'reverting a commit'
+ end
+
+ context 'when Gitaly revert feature is disabled', :disable_gitaly do
+ it_behaves_like 'reverting a commit'
+ end
end
describe '#cherry_pick' do
- let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
- let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
- let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
- let(:message) { 'cherry-pick message' }
-
- context 'when there is a conflict' do
- it 'raises an error' do
- expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ shared_examples 'cherry-picking a commit' do
+ let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
+ let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+ let(:message) { 'cherry-pick message' }
+
+ context 'when there is a conflict' do
+ it 'raises an error' do
+ expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
end
- end
- context 'when commit was already cherry-picked' do
- it 'raises an error' do
- repository.cherry_pick(user, pickable_commit, 'master', message)
+ context 'when commit was already cherry-picked' do
+ it 'raises an error' do
+ repository.cherry_pick(user, pickable_commit, 'master', message)
- expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
end
- end
- context 'when commit can be cherry-picked' do
- it 'cherry-picks the changes' do
- expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
+ context 'when commit can be cherry-picked' do
+ it 'cherry-picks the changes' do
+ expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
+ end
end
- end
- context 'cherry-picking a merge commit' do
- it 'cherry-picks the changes' do
- expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
+ context 'cherry-picking a merge commit' do
+ it 'cherry-picks the changes' do
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
- cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
+ cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
+ cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
- expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
- expect(cherry_pick_commit_message).to eq(message)
+ expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
+ expect(cherry_pick_commit_message).to eq(message)
+ end
end
end
+
+ context 'when Gitaly cherry_pick feature is enabled' do
+ it_behaves_like 'cherry-picking a commit'
+ end
+
+ context 'when Gitaly cherry_pick feature is disabled', :disable_gitaly do
+ it_behaves_like 'cherry-picking a commit'
+ end
end
describe '#before_delete' do
@@ -1898,6 +1922,23 @@ describe Repository do
File.delete(path)
end
+
+ it "attempting to call keep_around when exists a lock does not fail" do
+ ref = repository.send(:keep_around_ref_name, sample_commit.id)
+ path = File.join(repository.path, ref)
+ lock_path = "#{path}.lock"
+
+ FileUtils.mkdir_p(File.dirname(path))
+ File.open(lock_path, 'w') { |f| f.write('') }
+
+ begin
+ expect { repository.keep_around(sample_commit.id) }.not_to raise_error(Gitlab::Git::Repository::GitError)
+
+ expect(File.exist?(lock_path)).to be_falsey
+ ensure
+ File.delete(path)
+ end
+ end
end
describe '#update_ref' do
@@ -2325,4 +2366,111 @@ describe Repository do
end
end
end
+
+ describe '#contributors' do
+ let(:author_a) { build(:author, email: 'tiagonbotelho@hotmail.com', name: 'tiagonbotelho') }
+ let(:author_b) { build(:author, email: 'gitlab@winniehell.de', name: 'Winnie') }
+ let(:author_c) { build(:author, email: 'douwe@gitlab.com', name: 'Douwe Maan') }
+ let(:stubbed_commits) do
+ [build(:commit, author: author_a),
+ build(:commit, author: author_a),
+ build(:commit, author: author_b),
+ build(:commit, author: author_c),
+ build(:commit, author: author_c),
+ build(:commit, author: author_c)]
+ end
+ let(:order_by) { nil }
+ let(:sort) { nil }
+
+ before do
+ allow(repository).to receive(:commits).with(nil, limit: 2000, offset: 0, skip_merges: true).and_return(stubbed_commits)
+ end
+
+ subject { repository.contributors(order_by: order_by, sort: sort) }
+
+ def expect_contributors(*contributors)
+ expect(subject.map(&:email)).to eq(contributors.map(&:email))
+ end
+
+ it 'returns the array of Gitlab::Contributor for the repository' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+
+ context 'order_by email' do
+ let(:order_by) { 'email' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by email asc case insensitive' do
+ expect_contributors(author_c, author_b, author_a)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by email desc case insensitive' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+ end
+
+ context 'order_by name' do
+ let(:order_by) { 'name' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by name asc case insensitive' do
+ expect_contributors(author_c, author_a, author_b)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by name desc case insensitive' do
+ expect_contributors(author_b, author_a, author_c)
+ end
+ end
+ end
+
+ context 'order_by commits' do
+ let(:order_by) { 'commits' }
+
+ context 'asc' do
+ let(:sort) { 'asc' }
+
+ it 'returns all the contributors ordered by commits asc' do
+ expect_contributors(author_b, author_a, author_c)
+ end
+ end
+
+ context 'desc' do
+ let(:sort) { 'desc' }
+
+ it 'returns all the contributors ordered by commits desc' do
+ expect_contributors(author_c, author_a, author_b)
+ end
+ end
+ end
+
+ context 'invalid ordering' do
+ let(:order_by) { 'unknown' }
+
+ it 'returns the contributors unsorted' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+
+ context 'invalid sorting' do
+ let(:order_by) { 'name' }
+ let(:sort) { 'unknown' }
+
+ it 'returns the contributors unsorted' do
+ expect_contributors(author_a, author_b, author_c)
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index fece370c03f..ddad6862a63 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -87,6 +87,7 @@ describe Route do
end
context 'when conflicting redirects exist' do
+ let(:route) { create(:project).route }
let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
@@ -141,11 +142,50 @@ describe Route do
expect(redirect_route.source).to eq(route.source)
expect(redirect_route.path).to eq('foo')
end
+
+ context 'when the source is a Project' do
+ it 'creates a temporal RedirectRoute' do
+ project = create(:project)
+ route = project.route
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route.permanent?).to be_falsy
+ end
+ end
+
+ context 'when the source is not a project' do
+ it 'creates a permanent RedirectRoute' do
+ redirect_route = route.create_redirect('foo', permanent: true)
+ expect(redirect_route.permanent?).to be_truthy
+ end
+ end
end
describe '#delete_conflicting_redirects' do
+ context 'with permanent redirect' do
+ it 'does not delete the redirect' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+
+ expect do
+ route.delete_conflicting_redirects
+ end.not_to change { RedirectRoute.count }
+ end
+ end
+
+ context 'with temporal redirect' do
+ let(:route) { create(:project).route }
+
+ it 'deletes the redirect' do
+ route.create_redirect("#{route.path}/foo")
+
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
+ end
+ end
+
context 'when a redirect route with the same path exists' do
context 'when the redirect route has matching case' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path) }
it 'deletes the redirect' do
@@ -169,6 +209,7 @@ describe Route do
end
context 'when the redirect route is differently cased' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path.upcase) }
it 'deletes the redirect' do
@@ -185,7 +226,32 @@ describe Route do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end
+ context 'with permanent redirects' do
+ it 'does not return anything' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+ route.create_redirect("#{route.path}/foo/bar", permanent: true)
+ route.create_redirect("#{route.path}/baz/quz", permanent: true)
+
+ expect(route.conflicting_redirects).to be_empty
+ end
+ end
+
+ context 'with temporal redirects' do
+ let(:route) { create(:project).route }
+
+ it 'returns the redirect routes' do
+ route = create(:project).route
+ redirect1 = route.create_redirect("#{route.path}/foo")
+ redirect2 = route.create_redirect("#{route.path}/foo/bar")
+ redirect3 = route.create_redirect("#{route.path}/baz/quz")
+
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
+ end
+ end
+
context 'when a redirect route with the same path exists' do
+ let(:route) { create(:project).route }
+
context 'when the redirect route has matching case' do
let!(:redirect1) { route.create_redirect(route.path) }
@@ -214,4 +280,42 @@ describe Route do
end
end
end
+
+ describe "#conflicting_redirect_exists?" do
+ context 'when a conflicting redirect exists' do
+ let(:group1) { create(:group, path: 'foo') }
+ let(:group2) { create(:group, path: 'baz') }
+
+ it 'should not be saved' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+
+ expect(group2.save).to be_falsy
+ end
+
+ it 'should return an error on path' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+ group2.valid?
+ expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context 'when a conflicting redirect does not exist' do
+ let(:project1) { create(:project, path: 'foo') }
+ let(:project2) { create(:project, path: 'baz') }
+
+ it 'should be saved' do
+ project1.path = 'bar'
+ project1.save
+
+ project2.path = 'foo'
+ expect(project2.save).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b27c1b2cd1a..4687d9dfa00 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -913,11 +913,11 @@ describe User do
describe 'email matching' do
it 'returns users with a matching Email' do
- expect(described_class.search(user.email)).to eq([user, user2])
+ expect(described_class.search(user.email)).to eq([user])
end
- it 'returns users with a partially matching Email' do
- expect(described_class.search(user.email[0..2])).to eq([user, user2])
+ it 'does not return users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).not_to include(user, user2)
end
it 'returns users with a matching Email regardless of the casing' do
@@ -973,8 +973,8 @@ describe User do
expect(search_with_secondary_emails(user.email)).to eq([user])
end
- it 'returns users with a partially matching email' do
- expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+ it 'does not return users with a partially matching email' do
+ expect(search_with_secondary_emails(user.email[0..2])).not_to include([user])
end
it 'returns users with a matching email regardless of the casing' do
@@ -997,29 +997,8 @@ describe User do
expect(search_with_secondary_emails(email.email)).to eq([email.user])
end
- it 'returns users with a matching part of secondary email' do
- expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
- end
-
- it 'return users with a matching part of secondary email regardless of case' do
- expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
- expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
- end
-
- it 'returns multiple users with matching secondary emails' do
- email1 = create(:email, email: '1_testemail@example.com')
- email2 = create(:email, email: '2_testemail@example.com')
- email3 = create(:email, email: 'other@email.com')
- email3.user.update_attributes!(email: 'another@mail.com')
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).to include(email1.user.id, email2.user.id)
-
- expect(
- search_with_secondary_emails('testemail@example.com').map(&:id)
- ).not_to include(email3.user.id)
+ it 'does not return users with a matching part of secondary email' do
+ expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user])
end
end
@@ -2433,4 +2412,187 @@ describe User do
expect(user).not_to be_blocked
end
end
+
+ describe '#max_member_access_for_project_ids' do
+ shared_examples 'max member access for projects' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:owner_project) { create(:project, group: group) }
+ let(:master_project) { create(:project) }
+ let(:reporter_project) { create(:project) }
+ let(:developer_project) { create(:project) }
+ let(:guest_project) { create(:project) }
+ let(:no_access_project) { create(:project) }
+
+ let(:projects) do
+ [owner_project, master_project, reporter_project, developer_project, guest_project, no_access_project].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ owner_project.id => Gitlab::Access::OWNER,
+ master_project.id => Gitlab::Access::MASTER,
+ reporter_project.id => Gitlab::Access::REPORTER,
+ developer_project.id => Gitlab::Access::DEVELOPER,
+ guest_project.id => Gitlab::Access::GUEST,
+ no_access_project.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ create(:group_member, user: user, group: group)
+ master_project.add_master(user)
+ reporter_project.add_reporter(user)
+ developer_project.add_developer(user)
+ guest_project.add_guest(user)
+ end
+
+ it 'returns correct roles for different projects' do
+ expect(user.max_member_access_for_project_ids(projects)).to eq(expected)
+ end
+ end
+
+ context 'with RequestStore enabled', :request_store do
+ include_examples 'max member access for projects'
+
+ def access_levels(projects)
+ user.max_member_access_for_project_ids(projects)
+ end
+
+ it 'does not perform extra queries when asked for projects who have already been found' do
+ access_levels(projects)
+
+ expect { access_levels(projects) }.not_to exceed_query_limit(0)
+
+ expect(access_levels(projects)).to eq(expected)
+ end
+
+ it 'only requests the extra projects when uncached projects are passed' do
+ second_master_project = create(:project)
+ second_developer_project = create(:project)
+ second_master_project.add_master(user)
+ second_developer_project.add_developer(user)
+
+ all_projects = projects + [second_master_project.id, second_developer_project.id]
+
+ expected_all = expected.merge(second_master_project.id => Gitlab::Access::MASTER,
+ second_developer_project.id => Gitlab::Access::DEVELOPER)
+
+ access_levels(projects)
+
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_projects) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W(#{second_master_project.id}, #{second_developer_project.id})\W/)
+ expect(access_levels(all_projects)).to eq(expected_all)
+ end
+ end
+
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for projects'
+ end
+ end
+
+ describe '#max_member_access_for_group_ids' do
+ shared_examples 'max member access for groups' do
+ let(:user) { create(:user) }
+ let(:owner_group) { create(:group) }
+ let(:master_group) { create(:group) }
+ let(:reporter_group) { create(:group) }
+ let(:developer_group) { create(:group) }
+ let(:guest_group) { create(:group) }
+ let(:no_access_group) { create(:group) }
+
+ let(:groups) do
+ [owner_group, master_group, reporter_group, developer_group, guest_group, no_access_group].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ owner_group.id => Gitlab::Access::OWNER,
+ master_group.id => Gitlab::Access::MASTER,
+ reporter_group.id => Gitlab::Access::REPORTER,
+ developer_group.id => Gitlab::Access::DEVELOPER,
+ guest_group.id => Gitlab::Access::GUEST,
+ no_access_group.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ owner_group.add_owner(user)
+ master_group.add_master(user)
+ reporter_group.add_reporter(user)
+ developer_group.add_developer(user)
+ guest_group.add_guest(user)
+ end
+
+ it 'returns correct roles for different groups' do
+ expect(user.max_member_access_for_group_ids(groups)).to eq(expected)
+ end
+ end
+
+ context 'with RequestStore enabled', :request_store do
+ include_examples 'max member access for groups'
+
+ def access_levels(groups)
+ user.max_member_access_for_group_ids(groups)
+ end
+
+ it 'does not perform extra queries when asked for groups who have already been found' do
+ access_levels(groups)
+
+ expect { access_levels(groups) }.not_to exceed_query_limit(0)
+
+ expect(access_levels(groups)).to eq(expected)
+ end
+
+ it 'only requests the extra groups when uncached groups are passed' do
+ second_master_group = create(:group)
+ second_developer_group = create(:group)
+ second_master_group.add_master(user)
+ second_developer_group.add_developer(user)
+
+ all_groups = groups + [second_master_group.id, second_developer_group.id]
+
+ expected_all = expected.merge(second_master_group.id => Gitlab::Access::MASTER,
+ second_developer_group.id => Gitlab::Access::DEVELOPER)
+
+ access_levels(groups)
+
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_groups) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W(#{second_master_group.id}, #{second_developer_group.id})\W/)
+ expect(access_levels(all_groups)).to eq(expected_all)
+ end
+ end
+
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for groups'
+ end
+ end
+
+ describe "#username_previously_taken?" do
+ let(:user1) { create(:user, username: 'foo') }
+
+ context 'when the username has been taken before' do
+ before do
+ user1.username = 'bar'
+ user1.save!
+ end
+
+ 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/)
+ end
+ end
+
+ context 'when the username has not been taken before' do
+ it 'should be valid' do
+ expect(RedirectRoute.count).to eq(0)
+ user2 = build(:user, username: 'baz')
+ expect(user2).to be_valid
+ end
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 4f4e634829d..b4d25e06d9a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -9,6 +9,8 @@ describe GroupPolicy do
let(:admin) { create(:admin) }
let(:group) { create(:group) }
+ let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] }
+
let(:reporter_permissions) { [:admin_label] }
let(:developer_permissions) { [:admin_milestones] }
@@ -52,6 +54,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
+ expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -64,7 +67,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -76,7 +79,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -88,7 +91,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -100,7 +103,7 @@ describe GroupPolicy do
let(:current_user) { master }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -114,7 +117,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -128,7 +131,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -187,7 +190,7 @@ describe GroupPolicy do
let(:current_user) { nil }
it do
- expect_disallowed(:read_group)
+ expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -199,7 +202,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -211,7 +214,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -223,7 +226,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -235,7 +238,7 @@ describe GroupPolicy do
let(:current_user) { master }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -249,7 +252,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 48d4f3671c5..e96dbfb73c0 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do
it { is_expected.to include(cluster.provider.zone) }
it { is_expected.to include(cluster.name) }
end
+
+ describe '#can_toggle_cluster' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(cluster).to receive(:current_user).and_return(user)
+ end
+
+ subject { described_class.new(cluster).can_toggle_cluster? }
+
+ context 'when user can update' do
+ before do
+ allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(true)
+ end
+
+ context 'when cluster is created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when cluster is not created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when user can not update' do
+ before do
+ allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb
new file mode 100644
index 00000000000..c00e41725d9
--- /dev/null
+++ b/spec/presenters/group_member_presenter_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe GroupMemberPresenter do
+ let(:user) { double(:user) }
+ let(:group) { double(:group) }
+ let(:group_member) { double(:group_member, source: group) }
+ let(:presenter) { described_class.new(group_member, current_user: user) }
+
+ describe '#can_resend_invite?' do
+ context 'when group_member is invited' do
+ before do
+ expect(group_member).to receive(:invite?).and_return(true)
+ end
+
+ context 'and user can admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(true) }
+ end
+
+ context 'and user cannot admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+
+ context 'when group_member is not invited' do
+ before do
+ expect(group_member).to receive(:invite?).and_return(false)
+ end
+
+ context 'and user can admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+
+ context 'and user cannot admin_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+ end
+
+ describe '#can_update?' do
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_update?).to eq(true) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_update?).to eq(false) }
+ end
+ end
+
+ describe '#can_remove?' do
+ context 'when user can destroy_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_remove?).to eq(true) }
+ end
+
+ context 'when user cannot destroy_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_remove?).to eq(false) }
+ end
+ end
+
+ describe '#can_approve?' do
+ context 'when group_member has request an invite' do
+ before do
+ expect(group_member).to receive(:request?).and_return(true)
+ end
+
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(true) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+
+ context 'when group_member did not request an invite' do
+ before do
+ expect(group_member).to receive(:request?).and_return(false)
+ end
+
+ context 'when user can update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+
+ context 'when user cannot update_group_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 5e114434a67..f325d1776e4 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequestPresenter do
let(:pipeline) { build_stubbed(:ci_pipeline) }
before do
- allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ allow(resource).to receive(:actual_head_pipeline).and_return(pipeline)
end
context 'success with warnings' do
diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb
new file mode 100644
index 00000000000..83db5c56cdf
--- /dev/null
+++ b/spec/presenters/project_member_presenter_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe ProjectMemberPresenter do
+ let(:user) { double(:user) }
+ let(:project) { double(:project) }
+ let(:project_member) { double(:project_member, source: project) }
+ let(:presenter) { described_class.new(project_member, current_user: user) }
+
+ describe '#can_resend_invite?' do
+ context 'when project_member is invited' do
+ before do
+ expect(project_member).to receive(:invite?).and_return(true)
+ end
+
+ context 'and user can admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(true) }
+ end
+
+ context 'and user cannot admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+
+ context 'when project_member is not invited' do
+ before do
+ expect(project_member).to receive(:invite?).and_return(false)
+ end
+
+ context 'and user can admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+
+ context 'and user cannot admin_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false)
+ end
+
+ it { expect(presenter.can_resend_invite?).to eq(false) }
+ end
+ end
+ end
+
+ describe '#can_update?' do
+ context 'when user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_update?).to eq(true) }
+ end
+
+ context 'when user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_update?).to eq(false) }
+ end
+ end
+
+ describe '#can_remove?' do
+ context 'when user can destroy_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_remove?).to eq(true) }
+ end
+
+ context 'when user cannot destroy_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_remove?).to eq(false) }
+ end
+ end
+
+ describe '#can_approve?' do
+ context 'when project_member has request an invite' do
+ before do
+ expect(project_member).to receive(:request?).and_return(true)
+ end
+
+ context 'and user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(true) }
+ end
+
+ context 'and user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+
+ context 'when project_member did not request an invite' do
+ before do
+ expect(project_member).to receive(:request?).and_return(false)
+ end
+
+ context 'and user can update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+
+ context 'and user cannot update_project_member' do
+ before do
+ allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false)
+ end
+
+ it { expect(presenter.can_approve?).to eq(false) }
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb
index 3b858c40fd6..fe76f057115 100644
--- a/spec/requests/api/circuit_breakers_spec.rb
+++ b/spec/requests/api/circuit_breakers_spec.rb
@@ -47,7 +47,7 @@ describe API::CircuitBreakers do
describe 'DELETE circuit_breakers/repository_storage' do
it 'clears all circuit_breakers' do
- expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+ expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!)
delete api('/circuit_breakers/repository_storage', admin)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 04a658cd6c3..6330c140246 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -173,6 +173,28 @@ describe API::Groups do
end
describe "GET /groups/:id" do
+ # Given a group, create one project for each visibility level
+ #
+ # group - Group to add projects to
+ # share_with - If provided, each project will be shared with this Group
+ #
+ # Returns a Hash of visibility_level => Project pairs
+ def add_projects_to_group(group, share_with: nil)
+ projects = {
+ public: create(:project, :public, namespace: group),
+ internal: create(:project, :internal, namespace: group),
+ private: create(:project, :private, namespace: group)
+ }
+
+ if share_with
+ create(:project_group_link, project: projects[:public], group: share_with)
+ create(:project_group_link, project: projects[:internal], group: share_with)
+ create(:project_group_link, project: projects[:private], group: share_with)
+ end
+
+ projects
+ end
+
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
@@ -183,6 +205,26 @@ describe API::Groups do
get api("/groups/#{group1.id}")
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'returns only public projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}")
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
+
+ it 'returns only public projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}")
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id)
+ end
end
context "when authenticated as user" do
@@ -222,6 +264,26 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
+
+ it 'returns only public and internal projects in the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group)
+
+ get api("/groups/#{public_group.id}", user2)
+
+ expect(json_response['projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
+
+ it 'returns only public and internal projects shared with the group' do
+ public_group = create(:group, :public)
+ projects = add_projects_to_group(public_group, share_with: group1)
+
+ get api("/groups/#{group1.id}", user2)
+
+ expect(json_response['shared_projects'].map { |p| p['id'].to_i })
+ .to contain_exactly(projects[:public].id, projects[:internal].id)
+ end
end
context "when authenticated as admin" do
@@ -401,6 +463,20 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(404)
end
+
+ it 'avoids N+1 queries' do
+ get api("/groups/#{group1.id}/projects", admin)
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get api("/groups/#{group1.id}/projects", admin)
+ end.count
+
+ create(:project, namespace: group1)
+
+ expect do
+ get api("/groups/#{group1.id}/projects", admin)
+ end.not_to exceed_query_limit(control_count)
+ end
end
context 'when using group path in URL' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 67e1539cbc3..3c31980b273 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -537,16 +537,7 @@ describe API::Internal do
context 'the project path was changed' do
let!(:old_path_to_repo) { project.repository.path_to_repo }
- let!(:old_full_path) { project.full_path }
- let(:project_moved_message) do
- <<-MSG.strip_heredoc
- Project '#{old_full_path}' was moved to '#{project.full_path}'.
-
- Please update your Git remote and try again:
-
- git remote set-url origin #{project.ssh_url_to_repo}
- MSG
- end
+ let!(:repository) { project.repository }
before do
project.team << [user, :developer]
@@ -555,19 +546,17 @@ describe API::Internal do
end
it 'rejects the push' do
- push_with_path(key, old_path_to_repo)
+ push(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
it 'rejects the SSH pull' do
- pull_with_path(key, old_path_to_repo)
+ pull(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
end
end
@@ -695,7 +684,7 @@ describe API::Internal do
# end
# end
- describe 'POST /internal/post_receive' do
+ describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
let(:identifier) { 'key-123' }
let(:valid_params) do
@@ -713,6 +702,8 @@ describe API::Internal do
before do
project.team << [user, :developer]
+ allow(described_class).to receive(:identify).and_return(user)
+ allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
end
it 'enqueues a PostReceive worker job' do
@@ -780,6 +771,19 @@ describe API::Internal do
expect(json_response['broadcast_message']).to eq(nil)
end
end
+
+ context 'with a redirected data' do
+ it 'returns redirected message on the response' do
+ project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http')
+ project_moved.add_redirect_message
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response["redirected_message"]).to be_present
+ expect(json_response["redirected_message"]).to eq(project_moved.redirect_message)
+ end
+ end
end
describe 'POST /internal/pre_receive' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 99525cd0a6a..3f5070a1fd2 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -860,6 +860,20 @@ describe API::Issues, :mailer do
end
end
+ context 'user does not have permissions to create issue' do
+ let(:not_member) { create(:user) }
+
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'renders 403' do
+ post api("/projects/#{project.id}/issues", not_member), title: 'new issue'
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
it 'creates a new project issue' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2', weight: 3,
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 07d7f96bd70..10e6a3c07c8 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -95,6 +95,12 @@ describe API::ProtectedBranches do
describe 'POST /projects/:id/protected_branches' do
let(:branch_name) { 'new_branch' }
+ let(:post_endpoint) { api("/projects/#{project.id}/protected_branches", user) }
+
+ def expect_protection_to_be_successful
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(branch_name)
+ end
context 'when authenticated as a master' do
before do
@@ -102,7 +108,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch' do
- post api("/projects/#{project.id}/protected_branches", user), name: branch_name
+ post post_endpoint, name: branch_name
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -111,8 +117,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can push' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 30
+ post post_endpoint, name: branch_name, push_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -121,8 +126,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, merge_access_level: 30
+ post post_endpoint, name: branch_name, merge_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -131,8 +135,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can push and merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 30, merge_access_level: 30
+ post post_endpoint, name: branch_name, push_access_level: 30, merge_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -141,8 +144,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can push' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 0
+ post post_endpoint, name: branch_name, push_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -151,8 +153,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, merge_access_level: 0
+ post post_endpoint, name: branch_name, merge_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -161,8 +162,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can push or merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 0, merge_access_level: 0
+ post post_endpoint, name: branch_name, push_access_level: 0, merge_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -171,7 +171,8 @@ describe API::ProtectedBranches do
end
it 'returns a 409 error if the same branch is protected twice' do
- post api("/projects/#{project.id}/protected_branches", user), name: protected_name
+ post post_endpoint, name: protected_name
+
expect(response).to have_gitlab_http_status(409)
end
@@ -179,10 +180,9 @@ describe API::ProtectedBranches do
let(:branch_name) { 'feature/*' }
it "protects multiple branches with a wildcard in the name" do
- post api("/projects/#{project.id}/protected_branches", user), name: branch_name
+ post post_endpoint, name: branch_name
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(branch_name)
+ expect_protection_to_be_successful
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
end
@@ -195,7 +195,7 @@ describe API::ProtectedBranches do
end
it "returns a 403 error if guest" do
- post api("/projects/#{project.id}/protected_branches/", user), name: branch_name
+ post post_endpoint, name: branch_name
expect(response).to have_gitlab_http_status(403)
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 9f2ff3b5af6..741800ff61d 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -378,6 +378,28 @@ describe API::Repositories do
expect(first_contributor['additions']).to eq(0)
expect(first_contributor['deletions']).to eq(0)
end
+
+ context 'using sorting' do
+ context 'by commits desc' do
+ it 'returns the repository contribuors sorted by commits desc' do
+ get api(route, current_user), { order_by: 'commits', sort: 'desc' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('contributors')
+ expect(json_response.first['commits']).to be > json_response.last['commits']
+ end
+ end
+
+ context 'by name desc' do
+ it 'returns the repository contribuors sorted by name asc case insensitive' do
+ get api(route, current_user), { order_by: 'name', sort: 'asc' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('contributors')
+ expect(json_response.first['name'].downcase).to be < json_response.last['name'].downcase
+ end
+ end
+ end
end
context 'when unauthenticated', 'and project is public' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 47f4ccd4887..679d391caa5 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(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
end
context 'when job has been erased' do
@@ -985,15 +985,6 @@ describe API::Runner do
it_behaves_like 'successful artifacts upload'
end
- context 'when updates artifact' do
- before do
- upload_artifacts(file_upload2, headers_with_token)
- upload_artifacts(file_upload, headers_with_token)
- end
-
- it_behaves_like 'successful artifacts upload'
- end
-
context 'when using runners token' do
it 'responds with forbidden' do
upload_artifacts(file_upload, headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.project.runners_token))
@@ -1106,7 +1097,7 @@ describe API::Runner do
expect(response).to have_gitlab_http_status(201)
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
- expect(stored_artifacts_size).to eq(71759)
+ expect(stored_artifacts_size).to eq(72821)
end
end
@@ -1131,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(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
end
after do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 63175c40a18..015d4b9a491 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -54,7 +54,7 @@ describe API::Settings, 'Settings' do
dsa_key_restriction: 2048,
ecdsa_key_restriction: 384,
ed25519_key_restriction: 256,
- circuitbreaker_failure_wait_time: 2
+ circuitbreaker_check_interval: 2
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
@@ -75,7 +75,7 @@ describe API::Settings, 'Settings' do
expect(json_response['dsa_key_restriction']).to eq(2048)
expect(json_response['ecdsa_key_restriction']).to eq(384)
expect(json_response['ed25519_key_restriction']).to eq(256)
- expect(json_response['circuitbreaker_failure_wait_time']).to eq(2)
+ expect(json_response['circuitbreaker_check_interval']).to eq(2)
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 0bf7863bdc8..e2b19ad59f9 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,6 +16,44 @@ describe API::Tags do
describe 'GET /projects/:id/repository/tags' do
let(:route) { "/projects/#{project_id}/repository/tags" }
+ context 'sorting' do
+ let(:current_user) { user }
+
+ it 'sorts by descending order by default' do
+ get api(route, current_user)
+
+ desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id }
+
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags)
+ end
+
+ it 'sorts by ascending order if specified' do
+ get api("#{route}?sort=asc", current_user)
+
+ asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date }
+ asc_order_tags.map! { |tag| tag.dereferenced_target.id }
+
+ expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags)
+ end
+
+ it 'sorts by name in descending order when requested' do
+ get api("#{route}?order_by=name", current_user)
+
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
+ end
+
+ it 'sorts by name in ascending order when requested' do
+ get api("#{route}?order_by=name&sort=asc", current_user)
+
+ ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort
+
+ expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name)
+ end
+ end
+
shared_examples_for 'repository tags' do
it 'returns the repository tags' do
get api(route, current_user)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index a16f98bec36..fa02fffc82a 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -324,9 +324,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
@@ -533,9 +533,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
new file mode 100644
index 00000000000..7f406535dda
--- /dev/null
+++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/include_sidekiq_worker'
+
+describe RuboCop::Cop::IncludeSidekiqWorker do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `Sidekiq::Worker` is included' do
+ let(:source) { 'include Sidekiq::Worker' }
+ let(:correct_source) { 'include ApplicationWorker' }
+
+ it 'registers an offense ' do
+ inspect_source(cop, source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['Sidekiq::Worker'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(cop, source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb
new file mode 100644
index 00000000000..89112f01723
--- /dev/null
+++ b/spec/rubocop/cop/migration/remove_column_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/remove_column'
+
+describe RuboCop::Cop::Migration::RemoveColumn do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ def source(meth = 'change')
+ "def #{meth}; remove_column :table, :column; end"
+ end
+
+ context 'in a regular migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(false)
+ end
+
+ it 'registers an offense when remove_column is used in the change method' do
+ inspect_source(cop, source('change'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers an offense when remove_column is used in the up method' do
+ inspect_source(cop, source('up'))
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when remove_column is used in the down method' do
+ inspect_source(cop, source('down'))
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'in a post-deployment migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ allow(cop).to receive(:in_post_deployment_migration?).and_return(true)
+ end
+
+ it 'registers no offense' do
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of a migration' do
+ it 'registers no offense' do
+ inspect_source(cop, source)
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
new file mode 100644
index 00000000000..a31de381631
--- /dev/null
+++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/sidekiq_options_queue'
+
+describe RuboCop::Cop::SidekiqOptionsQueue do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'registers an offense when `sidekiq_options` is used with the `queue` option' do
+ inspect_source(cop, 'sidekiq_options queue: "some_queue"')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['queue: "some_queue"'])
+ end
+ end
+
+ it 'does not register an offense when `sidekiq_options` is used with another option' do
+ inspect_source(cop, 'sidekiq_options retry: false')
+
+ expect(cop.offenses).to be_empty
+ end
+end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index f9285049c0d..1ad672fd355 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -5,22 +5,34 @@ describe MergeRequestEntity do
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
- let(:request) { double('request', current_user: user) }
+ let(:request) { double('request', current_user: user, project: project) }
subject do
described_class.new(resource, request: request).as_json
end
- it 'includes pipeline' do
- req = double('request', current_user: user)
- pipeline = build_stubbed(:ci_pipeline)
- allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ describe 'pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
- pipeline_payload = PipelineDetailsEntity
- .represent(pipeline, request: req)
- .as_json
+ context 'when is up to date' do
+ let(:req) { double('request', current_user: user, project: project) }
- expect(subject[:pipeline]).to eq(pipeline_payload)
+ it 'returns pipeline' do
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eq(pipeline_payload)
+ end
+ end
+
+ context 'when is not up to date' do
+ it 'returns nil' do
+ pipeline.update(sha: "not up to date")
+
+ expect(subject[:pipeline]).to be_nil
+ end
+ end
end
it 'includes issues_links' do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 8fc1ceedc34..88d347322a6 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PipelineSerializer do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:serializer) do
described_class.new(current_user: user)
@@ -117,7 +117,7 @@ describe PipelineSerializer do
shared_examples 'no N+1 queries' do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(57)
+ expect(recorded.count).to be_within(1).of(36)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
index 5ec8ed0976d..090b2dcdd43 100644
--- a/spec/services/base_count_service_spec.rb
+++ b/spec/services/base_count_service_spec.rb
@@ -77,4 +77,10 @@ describe BaseCountService, :use_clean_rails_memory_store_caching do
expect { service.cache_key }.to raise_error(NotImplementedError)
end
end
+
+ describe '#cache_options' do
+ it 'returns the default in options' do
+ expect(service.cache_options).to eq({ raw: false })
+ end
+ end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 08847183bf4..267258b33a8 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -8,7 +8,7 @@ describe Ci::CreatePipelineService do
let(:ref_name) { 'refs/heads/master' }
before do
- stub_ci_pipeline_to_return_yaml_file
+ stub_repository_ci_yaml_file(sha: anything)
end
describe '#execute' do
@@ -44,6 +44,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to eq(project.pipelines.last)
expect(pipeline).to have_attributes(user: user)
expect(pipeline).to have_attributes(status: 'pending')
+ expect(pipeline.repository_source?).to be true
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
end
@@ -56,19 +57,51 @@ describe Ci::CreatePipelineService do
end
context 'when merge requests already exist for this source branch' do
- it 'updates head pipeline of each merge request' do
- merge_request_1 = create(:merge_request, source_branch: 'master',
- target_branch: "branch_1",
- source_project: project)
+ let(:merge_request_1) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
+ end
+ let(:merge_request_2) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
+ end
- merge_request_2 = create(:merge_request, source_branch: 'master',
- target_branch: "branch_2",
- source_project: project)
+ context 'when related merge request is already merged' do
+ let!(:merged_merge_request) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged')
+ end
+
+ it 'does not schedule update head pipeline job' do
+ expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id)
+
+ execute_service
+ end
+ end
+
+ context 'when the head pipeline sha equals merge request sha' do
+ it 'updates head pipeline of each merge request' do
+ merge_request_1
+ merge_request_2
- head_pipeline = execute_service
+ head_pipeline = execute_service
- expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
- expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
+ expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
+ expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
+ end
+ end
+
+ context 'when the head pipeline sha does not equal merge request sha' do
+ it 'does not update the head piepeline of MRs' do
+ merge_request_1
+ merge_request_2
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
+
+ expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error
+
+ last_pipeline = Ci::Pipeline.last
+
+ expect(merge_request_1.reload.head_pipeline).not_to eq(last_pipeline)
+ expect(merge_request_2.reload.head_pipeline).not_to eq(last_pipeline)
+ end
end
context 'when there is no pipeline for source branch' do
@@ -105,8 +138,7 @@ describe Ci::CreatePipelineService do
target_branch: "branch_1",
source_project: project)
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:latest?).and_return(false)
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
execute_service
@@ -498,5 +530,20 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ context 'when pipeline is running for a tag' do
+ before do
+ config = YAML.dump(test: { script: 'test', only: ['branches'] },
+ deploy: { script: 'deploy', only: ['tags'] })
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates a tagged pipeline' do
+ pipeline = execute_service(ref: 'v1.0.0')
+
+ expect(pipeline.tag?).to be true
+ end
+ end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 5ac30111ec9..de8a9ce12ff 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -15,16 +15,14 @@ module Ci
describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
- specific_runner.tag_list = ["linux"]
+ pending_job.update(tag_list: ["linux"])
+ specific_runner.update(tag_list: ["linux"])
expect(execute(specific_runner)).to eq(pending_job)
end
it "does not pick build with different tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
- specific_runner.tag_list = ["win32"]
+ pending_job.update(tag_list: ["linux"])
+ specific_runner.update(tag_list: ["win32"])
expect(execute(specific_runner)).to be_falsey
end
@@ -33,13 +31,12 @@ module Ci
end
it "does not pick build with tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
+ pending_job.update(tag_list: ["linux"])
expect(execute(specific_runner)).to be_falsey
end
it "pick build without tag" do
- specific_runner.tag_list = ["win32"]
+ specific_runner.update(tag_list: ["win32"])
expect(execute(specific_runner)).to eq(pending_job)
end
end
@@ -172,7 +169,7 @@ module Ci
context 'when first build is stalled' do
before do
- pending_job.lock_version = 10
+ pending_job.update(lock_version: 0)
end
subject { described_class.new(specific_runner).execute }
@@ -182,7 +179,7 @@ module Ci
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_job, other_build])
+ .and_return(Ci::Build.where(id: [pending_job, other_build]))
end
it "receives second build from the queue" do
@@ -194,7 +191,7 @@ module Ci
context 'when single build is in queue' do
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_job])
+ .and_return(Ci::Build.where(id: pending_job))
end
it "does not receive any valid result" do
@@ -205,7 +202,7 @@ module Ci
context 'when there is no build in queue' do
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([])
+ .and_return(Ci::Build.none)
end
it "does not receive builds but result is valid" do
@@ -279,6 +276,101 @@ module Ci
end
end
+ context 'when "dependencies" keyword is specified' do
+ shared_examples 'not pick' do
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_missing_dependency_failure
+ end
+ end
+
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect(subject).to eq(pending_job) }
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it_behaves_like 'not pick'
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it_behaves_like 'not pick'
+ end
+
+ context 'when job object is staled' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:drop!)
+ .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!))
+ end
+
+ it 'does not drop nor pick' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect(subject).to eq(pending_job) }
+ end
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect(subject).to eq(pending_job) }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect(subject).to eq(pending_job) }
+ end
+ end
+
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['test'] } ) }
+
+ subject { execute(specific_runner) }
+
+ context 'when validates for dependencies is enabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ it_behaves_like 'validation is active'
+ end
+
+ context 'when validates for dependencies is disabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: true)
+ end
+
+ it_behaves_like 'validation is not active'
+ end
+ end
+
def execute(runner)
described_class.new(runner).execute.build
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index b61d1cb765e..d48a44fa57f 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -17,7 +17,7 @@ describe Ci::RetryBuildService do
%i[id status user token coverage trace runner artifacts_expire_at
artifacts_file artifacts_metadata artifacts_size created_at
updated_at started_at finished_at queued_at erased_by
- erased_at auto_canceled_by].freeze
+ erased_at auto_canceled_by job_artifacts job_artifacts_archive job_artifacts_metadata].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
@@ -34,7 +34,7 @@ describe Ci::RetryBuildService do
end
let(:build) do
- create(:ci_build, :failed, :artifacts_expired, :erased,
+ create(:ci_build, :failed, :artifacts, :expired, :erased,
:queued, :coverage, :tags, :allowed_to_fail, :on_tag,
:triggered, :trace, :teardown_environment,
description: 'my-job', stage: 'test', pipeline: pipeline,
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
index cf95361c935..047a6e44dab 100644
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ b/spec/services/clusters/applications/schedule_installation_service_spec.rb
@@ -22,6 +22,8 @@ describe Clusters::Applications::ScheduleInstallationService do
let(:service) { described_class.new(project, nil, cluster: cluster, application_class: application_class) }
it 'creates a new application' do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+
expect { service.execute }.to change { application_class.count }.by(1)
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 5b6edb73beb..e2e64659dfa 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -4,10 +4,11 @@ describe Clusters::CreateService do
let(:access_token) { 'xxx' }
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do
- context 'when correct params' do
+ shared_context 'valid params' do
let(:params) do
{
name: 'test-cluster',
@@ -20,27 +21,9 @@ describe Clusters::CreateService do
}
}
end
-
- it 'creates a cluster object and performs a worker' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
-
- expect { result }
- .to change { Clusters::Cluster.count }.by(1)
- .and change { Clusters::Providers::Gcp.count }.by(1)
-
- expect(result.name).to eq('test-cluster')
- expect(result.user).to eq(user)
- expect(result.project).to eq(project)
- expect(result.provider.gcp_project_id).to eq('gcp-project')
- expect(result.provider.zone).to eq('us-central1-a')
- expect(result.provider.num_nodes).to eq(1)
- expect(result.provider.machine_type).to eq('machine_type-a')
- expect(result.provider.access_token).to eq(access_token)
- expect(result.platform).to be_nil
- end
end
- context 'when invalid params' do
+ shared_context 'invalid params' do
let(:params) do
{
name: 'test-cluster',
@@ -53,11 +36,57 @@ describe Clusters::CreateService do
}
}
end
+ end
+
+ shared_examples 'create cluster' do
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+ expect(subject.name).to eq('test-cluster')
+ expect(subject.user).to eq(user)
+ expect(subject.project).to eq(project)
+ expect(subject.provider.gcp_project_id).to eq('gcp-project')
+ expect(subject.provider.zone).to eq('us-central1-a')
+ expect(subject.provider.num_nodes).to eq(1)
+ expect(subject.provider.machine_type).to eq('machine_type-a')
+ expect(subject.provider.access_token).to eq(access_token)
+ expect(subject.platform).to be_nil
+ end
+ end
+
+ shared_examples 'error' do
it 'returns an error' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Clusters::Cluster.count }.by(0)
- expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ expect { subject }.to change { Clusters::Cluster.count }.by(0)
+ expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+
+ context 'when project has no clusters' do
+ context 'when correct params' do
+ include_context 'valid params'
+
+ include_examples 'create cluster'
+ end
+
+ context 'when invalid params' do
+ include_context 'invalid params'
+
+ include_examples 'error'
+ end
+ end
+
+ context 'when project has a cluster' do
+ include_context 'valid params'
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ it 'does not create a cluster' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
end
end
end
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
index 2d04d824180..d4ef31c0c74 100644
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ b/spec/services/members/authorized_destroy_service_spec.rb
@@ -45,7 +45,7 @@ describe Members::AuthorizedDestroyService do
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
- expect(issue.reload.assignee_id).to be_nil
+ expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index fee293760f5..b5c92e681fb 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -39,6 +39,7 @@ describe MergeRequests::BuildService do
describe '#execute' do
it 'calls the compare service with the correct arguments' do
+ allow_any_instance_of(described_class).to receive(:branches_valid?).and_return(true)
expect(CompareService).to receive(:new)
.with(project, Gitlab::Git::BRANCH_REF_PREFIX + source_branch)
.and_call_original
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index a7ab389b357..623b182b205 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -100,5 +100,17 @@ describe MergeRequests::CreateFromIssueService do
expect(result[:merge_request].target_branch).to eq(project.default_branch)
end
+
+ it 'executes quick actions if the build service sets them in the description' do
+ allow(service).to receive(:merge_request).and_wrap_original do |m, *args|
+ m.call(*args).tap do |merge_request|
+ merge_request.description = "/assign #{user.to_reference}"
+ end
+ end
+
+ result = service.execute
+
+ expect(result[:merge_request].assignee).to eq(user)
+ end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index f86f1ac2443..c38ddf4612b 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
describe MergeRequests::MergeService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) }
let(:project) { merge_request.project }
before do
- project.team << [user, :master]
- project.team << [user2, :developer]
+ project.add_master(user)
+ project.add_developer(user2)
end
describe '#execute' do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index a2c05761f6b..61ec4709c59 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -74,6 +74,20 @@ describe MergeRequests::RefreshService do
end
end
+ context 'when pipeline exists for the source branch' do
+ let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)}
+
+ subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+
+ it 'updates the head_pipeline_id for @merge_request' do
+ expect { subject }.to change { @merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id)
+ end
+
+ it 'does not update the head_pipeline_id for @fork_merge_request' do
+ expect { subject }.not_to change { @fork_merge_request.reload.head_pipeline_id }
+ end
+ end
+
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index db5de572b6d..43e2643f709 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -12,6 +12,8 @@ describe NotificationService, :mailer do
shared_examples 'notifications for new mentions' do
def send_notifications(*new_mentions)
+ mentionable.description = new_mentions.map(&:to_reference).join(' ')
+
notification.send(notification_method, mentionable, new_mentions, @u_disabled)
end
@@ -20,13 +22,13 @@ describe NotificationService, :mailer do
should_not_email_anyone
end
- it 'emails new mentions with a watch level higher than participant' do
- send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global)
- should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ it 'emails new mentions with a watch level higher than mention' do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned)
+ should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned)
end
- it 'does not email new mentions with a watch level equal to or less than participant' do
- send_notifications(@u_participating, @u_mentioned)
+ it 'does not email new mentions with a watch level equal to or less than mention' do
+ send_notifications(@u_disabled)
should_not_email_anyone
end
end
@@ -509,6 +511,14 @@ describe NotificationService, :mailer do
should_not_email(issue.assignees.first)
end
+ it "emails any mentioned users with the mention level" do
+ issue.description = @u_mentioned.to_reference
+
+ notification.new_issue(issue, @u_disabled)
+
+ should_email(@u_mentioned)
+ end
+
it "emails the author if they've opted into notifications about their activity" do
issue.author.notified_of_own_activity = true
@@ -900,6 +910,14 @@ describe NotificationService, :mailer do
should_not_email(@u_lazy_participant)
end
+ it "emails any mentioned users with the mention level" do
+ merge_request.description = @u_mentioned.to_reference
+
+ notification.new_merge_request(merge_request, @u_disabled)
+
+ should_email(@u_mentioned)
+ end
+
it "emails the author if they've opted into notifications about their activity" do
merge_request.author.notified_of_own_activity = true
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
index cc496501bad..183f6128c7b 100644
--- a/spec/services/projects/count_service_spec.rb
+++ b/spec/services/projects/count_service_spec.rb
@@ -4,9 +4,17 @@ describe Projects::CountService do
let(:project) { build(:project, id: 1) }
let(:service) { described_class.new(project) }
- describe '#relation_for_count' do
+ describe '.query' do
it 'raises NotImplementedError' do
- expect { service.relation_for_count }.to raise_error(NotImplementedError)
+ expect { described_class.query(project.id) }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#relation_for_count' do
+ it 'calls the class method query with the project id' do
+ expect(described_class).to receive(:query).with(project.id)
+
+ service.relation_for_count
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 53862283a27..4057caca2ac 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -3,210 +3,253 @@ require 'spec_helper'
describe Projects::ForkService do
include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new }
+ context 'when forking a new project' do
+ describe 'fork by user' do
+ before do
+ @from_user = create(:user)
+ @from_namespace = @from_user.namespace
+ avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+ @from_project = create(:project,
+ :repository,
+ creator_id: @from_user.id,
+ namespace: @from_namespace,
+ star_count: 107,
+ avatar: avatar,
+ description: 'wow such project')
+ @to_user = create(:user)
+ @to_namespace = @to_user.namespace
+ @from_project.add_user(@to_user, :developer)
+ end
- describe 'fork by user' do
- before do
- @from_user = create(:user)
- @from_namespace = @from_user.namespace
- avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
- @from_project = create(:project,
- :repository,
- creator_id: @from_user.id,
- namespace: @from_namespace,
- star_count: 107,
- avatar: avatar,
- description: 'wow such project')
- @to_user = create(:user)
- @to_namespace = @to_user.namespace
- @from_project.add_user(@to_user, :developer)
- end
+ context 'fork project' do
+ context 'when forker is a guest' do
+ before do
+ @guest = create(:user)
+ @from_project.add_user(@guest, :guest)
+ end
+ subject { fork_project(@from_project, @guest) }
- context 'fork project' do
- context 'when forker is a guest' do
- before do
- @guest = create(:user)
- @from_project.add_user(@guest, :guest)
+ it { is_expected.not_to be_persisted }
+ it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
end
- subject { fork_project(@from_project, @guest) }
- it { is_expected.not_to be_persisted }
- it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
- end
+ describe "successfully creates project in the user namespace" do
+ let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
- describe "successfully creates project in the user namespace" do
- let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
-
- it { expect(to_project).to be_persisted }
- it { expect(to_project.errors).to be_empty }
- it { expect(to_project.owner).to eq(@to_user) }
- it { expect(to_project.namespace).to eq(@to_user.namespace) }
- it { expect(to_project.star_count).to be_zero }
- it { expect(to_project.description).to eq(@from_project.description) }
- it { expect(to_project.avatar.file).to be_exists }
-
- # This test is here because we had a bug where the from-project lost its
- # avatar after being forked.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
- it "after forking the from-project still has its avatar" do
- # If we do not fork the project first we cannot detect the bug.
- expect(to_project).to be_persisted
-
- expect(@from_project.avatar.file).to be_exists
- end
+ it { expect(to_project).to be_persisted }
+ it { expect(to_project.errors).to be_empty }
+ it { expect(to_project.owner).to eq(@to_user) }
+ it { expect(to_project.namespace).to eq(@to_user.namespace) }
+ it { expect(to_project.star_count).to be_zero }
+ it { expect(to_project.description).to eq(@from_project.description) }
+ it { expect(to_project.avatar.file).to be_exists }
- it 'flushes the forks count cache of the source project' do
- expect(@from_project.forks_count).to be_zero
+ # This test is here because we had a bug where the from-project lost its
+ # avatar after being forked.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
+ it "after forking the from-project still has its avatar" do
+ # If we do not fork the project first we cannot detect the bug.
+ expect(to_project).to be_persisted
- fork_project(@from_project, @to_user)
+ expect(@from_project.avatar.file).to be_exists
+ end
- expect(@from_project.forks_count).to eq(1)
- end
+ it 'flushes the forks count cache of the source project' do
+ expect(@from_project.forks_count).to be_zero
- it 'creates a fork network with the new project and the root project set' do
- to_project
- fork_network = @from_project.reload.fork_network
+ fork_project(@from_project, @to_user)
- expect(fork_network).not_to be_nil
- expect(fork_network.root_project).to eq(@from_project)
- expect(fork_network.projects).to contain_exactly(@from_project, to_project)
- end
- end
+ expect(@from_project.forks_count).to eq(1)
+ end
- context 'creating a fork of a fork' do
- let(:from_forked_project) { fork_project(@from_project, @to_user) }
- let(:other_namespace) do
- group = create(:group)
- group.add_owner(@to_user)
- group
- end
- let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+ it 'creates a fork network with the new project and the root project set' do
+ to_project
+ fork_network = @from_project.reload.fork_network
- it 'sets the root of the network to the root project' do
- expect(to_project.fork_network.root_project).to eq(@from_project)
+ expect(fork_network).not_to be_nil
+ expect(fork_network.root_project).to eq(@from_project)
+ expect(fork_network.projects).to contain_exactly(@from_project, to_project)
+ end
end
- it 'sets the forked_from_project on the membership' do
- expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ context 'creating a fork of a fork' do
+ let(:from_forked_project) { fork_project(@from_project, @to_user) }
+ let(:other_namespace) do
+ group = create(:group)
+ group.add_owner(@to_user)
+ group
+ end
+ let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+
+ it 'sets the root of the network to the root project' do
+ expect(to_project.fork_network.root_project).to eq(@from_project)
+ end
+
+ it 'sets the forked_from_project on the membership' do
+ expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ end
end
end
- end
- context 'project already exists' do
- it "fails due to validation, not transaction failure" do
- @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
- @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
- expect(@existing_project).to be_persisted
+ context 'project already exists' do
+ it "fails due to validation, not transaction failure" do
+ @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
+ @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
+ expect(@existing_project).to be_persisted
- expect(@to_project).not_to be_persisted
- expect(@to_project.errors[:name]).to eq(['has already been taken'])
- expect(@to_project.errors[:path]).to eq(['has already been taken'])
+ expect(@to_project).not_to be_persisted
+ expect(@to_project.errors[:name]).to eq(['has already been taken'])
+ expect(@to_project.errors[:path]).to eq(['has already been taken'])
+ end
end
- end
- context 'repository already exists' do
- let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ context 'repository already exists' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
- before do
- gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
- end
+ before do
+ gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
- end
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
- it 'does not allow creation' do
- to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
+ it 'does not allow creation' do
+ to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
- expect(to_project).not_to be_persisted
- expect(to_project.errors.messages).to have_key(:base)
- expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(to_project).not_to be_persisted
+ expect(to_project.errors.messages).to have_key(:base)
+ expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
- end
- context 'GitLab CI is enabled' do
- it "forks and enables CI for fork" do
- @from_project.enable_ci
- @to_project = fork_project(@from_project, @to_user)
- expect(@to_project.builds_enabled?).to be_truthy
+ context 'GitLab CI is enabled' do
+ it "forks and enables CI for fork" do
+ @from_project.enable_ci
+ @to_project = fork_project(@from_project, @to_user)
+ expect(@to_project.builds_enabled?).to be_truthy
+ end
end
- end
- context "when project has restricted visibility level" do
- context "and only one visibility level is restricted" do
- before do
- @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ context "when project has restricted visibility level" do
+ context "and only one visibility level is restricted" do
+ before do
+ @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ it "creates fork with highest allowed level" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
- it "creates fork with highest allowed level" do
- forked_project = fork_project(@from_project, @to_user)
+ context "and all visibility levels are restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ end
+
+ it "creates fork with private visibility levels" do
+ forked_project = fork_project(@from_project, @to_user)
- expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
end
end
+ end
- context "and all visibility levels are restricted" do
- before do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ describe 'fork to namespace' do
+ before do
+ @group_owner = create(:user)
+ @developer = create(:user)
+ @project = create(:project, :repository,
+ creator_id: @group_owner.id,
+ star_count: 777,
+ description: 'Wow, such a cool project!')
+ @group = create(:group)
+ @group.add_user(@group_owner, GroupMember::OWNER)
+ @group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
+ @opts = { namespace: @group }
+ end
+
+ context 'fork project for group' do
+ it 'group owner successfully forks project into the group' do
+ to_project = fork_project(@project, @group_owner, @opts)
+
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
+ expect(to_project.owner).to eq(@group)
+ expect(to_project.namespace).to eq(@group)
+ expect(to_project.name).to eq(@project.name)
+ expect(to_project.path).to eq(@project.path)
+ expect(to_project.description).to eq(@project.description)
+ expect(to_project.star_count).to be_zero
end
+ end
- it "creates fork with private visibility levels" do
- forked_project = fork_project(@from_project, @to_user)
+ context 'fork project for group when user not owner' do
+ it 'group developer fails to fork project into the group' do
+ to_project = fork_project(@project, @developer, @opts)
+ expect(to_project.errors[:namespace]).to eq(['is not valid'])
+ end
+ end
- expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ context 'project already exists in group' do
+ it 'fails due to validation, not transaction failure' do
+ existing_project = create(:project, :repository,
+ name: @project.name,
+ namespace: @group)
+ to_project = fork_project(@project, @group_owner, @opts)
+ expect(existing_project.persisted?).to be_truthy
+ expect(to_project.errors[:name]).to eq(['has already been taken'])
+ expect(to_project.errors[:path]).to eq(['has already been taken'])
end
end
end
end
- describe 'fork to namespace' do
- before do
- @group_owner = create(:user)
- @developer = create(:user)
- @project = create(:project, :repository,
- creator_id: @group_owner.id,
- star_count: 777,
- description: 'Wow, such a cool project!')
- @group = create(:group)
- @group.add_user(@group_owner, GroupMember::OWNER)
- @group.add_user(@developer, GroupMember::DEVELOPER)
- @project.add_user(@developer, :developer)
- @project.add_user(@group_owner, :developer)
- @opts = { namespace: @group }
+ context 'when linking fork to an existing project' do
+ let(:fork_from_project) { create(:project, :public) }
+ let(:fork_to_project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(fork_from_project, user) }
+
+ def forked_from_project(project)
+ project.fork_network_member&.forked_from_project
end
- context 'fork project for group' do
- it 'group owner successfully forks project into the group' do
- to_project = fork_project(@project, @group_owner, @opts)
-
- expect(to_project).to be_persisted
- expect(to_project.errors).to be_empty
- expect(to_project.owner).to eq(@group)
- expect(to_project.namespace).to eq(@group)
- expect(to_project.name).to eq(@project.name)
- expect(to_project.path).to eq(@project.path)
- expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ context 'if project is already forked' do
+ it 'does not create fork relation' do
+ allow(fork_to_project).to receive(:forked?).and_return(true)
+ expect(forked_from_project(fork_to_project)).to be_nil
+ expect(subject.execute(fork_to_project)).to be_nil
+ expect(forked_from_project(fork_to_project)).to be_nil
end
end
- context 'fork project for group when user not owner' do
- it 'group developer fails to fork project into the group' do
- to_project = fork_project(@project, @developer, @opts)
- expect(to_project.errors[:namespace]).to eq(['is not valid'])
+ context 'if project is not forked' do
+ it 'creates fork relation' do
+ expect(fork_to_project.forked?).to be false
+ expect(forked_from_project(fork_to_project)).to be_nil
+
+ subject.execute(fork_to_project)
+
+ expect(fork_to_project.forked?).to be true
+ expect(forked_from_project(fork_to_project)).to eq fork_from_project
+ expect(fork_to_project.forked_from_project).to eq fork_from_project
end
- end
- context 'project already exists in group' do
- it 'fails due to validation, not transaction failure' do
- existing_project = create(:project, :repository,
- name: @project.name,
- namespace: @group)
- to_project = fork_project(@project, @group_owner, @opts)
- expect(existing_project.persisted?).to be_truthy
- expect(to_project.errors[:name]).to eq(['has already been taken'])
- expect(to_project.errors[:path]).to eq(['has already been taken'])
+ it 'flushes the forks count cache of the source project' do
+ expect(fork_from_project.forks_count).to be_zero
+
+ subject.execute(fork_to_project)
+
+ expect(fork_from_project.forks_count).to eq(1)
end
end
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index d4ac1f6ad81..bfb86284d86 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -1,10 +1,18 @@
require "spec_helper"
describe Projects::UpdatePagesService do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
- let(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') }
+ set(:project) { create(:project, :repository) }
+ set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
+ set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') }
let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
+ let(:extension) { 'zip' }
+
+ let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{extension}") }
+ let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{extension}") }
+ let(:metadata) do
+ filename = Rails.root + "spec/fixtures/pages.#{extension}.meta"
+ fixture_file_upload(filename) if File.exist?(filename)
+ end
subject { described_class.new(project, build) }
@@ -12,18 +20,85 @@ describe Projects::UpdatePagesService do
project.remove_pages
end
- %w(tar.gz zip).each do |format|
- context "for valid #{format}" do
- let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") }
- let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") }
- let(:metadata) do
- filename = Rails.root + "spec/fixtures/pages.#{format}.meta"
- fixture_file_upload(filename) if File.exist?(filename)
+ context 'legacy artifacts' do
+ %w(tar.gz zip).each do |format|
+ let(:extension) { format }
+
+ context "for valid #{format}" do
+ before do
+ build.update_attributes(legacy_artifacts_file: file)
+ build.update_attributes(legacy_artifacts_metadata: metadata)
+ end
+
+ describe 'pages artifacts' do
+ context 'with expiry date' do
+ before do
+ build.artifacts_expire_in = "2 days"
+ end
+
+ it "doesn't delete artifacts" do
+ expect(execute).to eq(:success)
+
+ expect(build.reload.artifacts?).to eq(true)
+ end
+ end
+
+ context 'without expiry date' do
+ it "does delete artifacts" do
+ expect(execute).to eq(:success)
+
+ expect(build.reload.artifacts?).to eq(false)
+ end
+ end
+ end
+
+ it 'succeeds' do
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+
+ # Check that all expected files are extracted
+ %w[index.html zero .hidden/file].each do |filename|
+ expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy
+ end
+ end
+
+ it 'limits pages size' do
+ stub_application_setting(max_pages_size: 1)
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'removes pages after destroy' do
+ expect(PagesWorker).to receive(:perform_in)
+ expect(project.pages_deployed?).to be_falsey
+ expect(execute).to eq(:success)
+ expect(project.pages_deployed?).to be_truthy
+ project.destroy
+ expect(project.pages_deployed?).to be_falsey
+ end
+
+ it 'fails if sha on branch is not latest' do
+ build.update_attributes(ref: 'feature')
+
+ expect(execute).not_to eq(:success)
+ end
+
+ it 'fails for empty file fails' do
+ build.update_attributes(legacy_artifacts_file: empty_file)
+
+ expect(execute).not_to eq(:success)
+ end
end
+ end
+ end
+ context 'for new artifacts' do
+ context "for a valid job" do
before do
- build.update_attributes(artifacts_file: file)
- build.update_attributes(artifacts_metadata: metadata)
+ create(:ci_job_artifact, file: file, job: build)
+ create(:ci_job_artifact, file_type: :metadata, file: metadata, job: build)
+
+ build.reload
end
describe 'pages artifacts' do
@@ -35,7 +110,7 @@ describe Projects::UpdatePagesService do
it "doesn't delete artifacts" do
expect(execute).to eq(:success)
- expect(build.reload.artifacts_file?).to eq(true)
+ expect(build.artifacts?).to eq(true)
end
end
@@ -43,7 +118,7 @@ describe Projects::UpdatePagesService do
it "does delete artifacts" do
expect(execute).to eq(:success)
- expect(build.reload.artifacts_file?).to eq(false)
+ expect(build.reload.artifacts?).to eq(false)
end
end
end
@@ -74,13 +149,14 @@ describe Projects::UpdatePagesService do
end
it 'fails if sha on branch is not latest' do
- pipeline.update_attributes(sha: 'old_sha')
- build.update_attributes(artifacts_file: file)
+ build.update_attributes(ref: 'feature')
+
expect(execute).not_to eq(:success)
end
it 'fails for empty file fails' do
- build.update_attributes(artifacts_file: empty_file)
+ build.job_artifacts_archive.update_attributes(file: empty_file)
+
expect(execute).not_to eq(:success)
end
end
@@ -97,7 +173,7 @@ describe Projects::UpdatePagesService do
end
it 'fails for invalid archive' do
- build.update_attributes(artifacts_file: invalid_file)
+ build.update_attributes(legacy_artifacts_file: invalid_file)
expect(execute).not_to eq(:success)
end
@@ -108,8 +184,8 @@ describe Projects::UpdatePagesService do
file = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip')
metafile = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta')
- build.update_attributes(artifacts_file: file)
- build.update_attributes(artifacts_metadata: metafile)
+ build.update_attributes(legacy_artifacts_file: file)
+ build.update_attributes(legacy_artifacts_metadata: metafile)
allow(build).to receive(:artifacts_metadata_entry)
.and_return(metadata)
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index fcd71857af3..d887f70efae 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -199,24 +199,53 @@ describe Projects::UpdateService do
end
describe '#run_auto_devops_pipeline?' do
- subject { described_class.new(project, user, params).run_auto_devops_pipeline? }
+ subject { described_class.new(project, user).run_auto_devops_pipeline? }
- context 'when neither pipeline setting is true' do
- let(:params) { {} }
+ context 'when master contains a .gitlab-ci.yml file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
+ end
it { is_expected.to eq(false) }
end
- context 'when run_auto_devops_pipeline_explicit is true' do
- let(:params) { { run_auto_devops_pipeline_explicit: 'true' } }
+ context 'when auto devops is explicitly enabled' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
it { is_expected.to eq(true) }
end
- context 'when run_auto_devops_pipeline_implicit is true' do
- let(:params) { { run_auto_devops_pipeline_implicit: 'true' } }
+ context 'when auto devops is explicitly disabled' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ end
- it { is_expected.to eq(true) }
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is set to instance setting' do
+ before do
+ project.create_auto_devops!(enabled: nil)
+ allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true)
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is disabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a918383ecd2..47412110b4b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -692,9 +692,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 = '* 12345678 - &lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
+ escaped = '&lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
- expect(described_class.new_commit_summary([commit])).to eq([escaped])
+ expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}]))
end
end
diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb
index a188cf86772..bee8380e8b7 100644
--- a/spec/services/users/keys_count_service_spec.rb
+++ b/spec/services/users/keys_count_service_spec.rb
@@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do
expect(service.count).to eq(1)
end
- it 'caches the number of keys in Redis' do
+ it 'caches the number of keys in Redis', :request_store do
+ service.delete_cache
+ control_count = ActiveRecord::QueryRecorder.new { service.count }.count
service.delete_cache
- recorder = ActiveRecord::QueryRecorder.new do
- 2.times { service.count }
- end
-
- expect(recorder.count).to eq(1)
+ expect { 2.times { service.count } }.not_to exceed_query_limit(control_count)
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index a669429ce3e..21910e69d2e 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -146,7 +146,7 @@ describe WebHookService do
let(:system_hook) { create(:system_hook) }
it 'enqueue WebHookWorker' do
- expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks')
+ expect(WebHookWorker).to receive(:perform_async).with(project_hook.id, data, 'push_hooks')
described_class.new(project_hook, data, 'push_hooks').async_execute
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 6310ea1b52b..f94fb8733d5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -121,18 +121,6 @@ RSpec.configure do |config|
reset_delivered_emails!
end
- # Stub the `ForkedStorageCheck.storage_available?` method unless
- # `:broken_storage` metadata is defined
- #
- # This check can be slow and is unnecessary in a test environment where we
- # know the storage is available, because we create it at runtime
- config.before(:example) do |example|
- unless example.metadata[:broken_storage]
- allow(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).and_return(true)
- end
- end
-
config.around(:each, :use_clean_rails_memory_store_caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
@@ -207,3 +195,6 @@ Shoulda::Matchers.configure do |config|
with.library :rails
end
end
+
+# Prevent Rugged from picking up local developer gitconfig.
+Rugged::Settings['search_path_global'] = Rails.root.join('tmp/tests').to_s
diff --git a/spec/support/batch_loader.rb b/spec/support/batch_loader.rb
new file mode 100644
index 00000000000..bb790e660a6
--- /dev/null
+++ b/spec/support/batch_loader.rb
@@ -0,0 +1,5 @@
+RSpec.configure do |config|
+ config.after do
+ BatchLoader::Executor.clear_current
+ end
+end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 9f672bc92fc..935b170a0f6 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -7,21 +7,41 @@ require 'selenium-webdriver'
# Give CI some extra time
timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
-Capybara.javascript_driver = :chrome
Capybara.register_driver :chrome do |app|
- extra_args = []
- extra_args << 'headless' unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
-
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
- chromeOptions: {
- 'args' => %w[no-sandbox disable-gpu --window-size=1240,1400] + extra_args
+ # This enables access to logs with `page.driver.manage.get_log(:browser)`
+ loggingPrefs: {
+ browser: "ALL",
+ client: "ALL",
+ driver: "ALL",
+ server: "ALL"
}
)
- Capybara::Selenium::Driver
- .new(app, browser: :chrome, desired_capabilities: capabilities)
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument("window-size=1240,1400")
+
+ # Chrome won't work properly in a Docker container in sandbox mode
+ options.add_argument("no-sandbox")
+
+ # Run headless by default unless CHROME_HEADLESS specified
+ unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i
+ options.add_argument("headless")
+
+ # Chrome documentation says this flag is needed for now
+ # https://developers.google.com/web/updates/2017/04/headless-chrome#cli
+ options.add_argument("disable-gpu")
+ end
+
+ Capybara::Selenium::Driver.new(
+ app,
+ browser: :chrome,
+ desired_capabilities: capabilities,
+ options: options
+ )
end
+Capybara.javascript_driver = :chrome
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index dabf0db7666..8a073e58db8 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -63,7 +63,7 @@ module GoogleApi
##
# gcloud container clusters create
- # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.zones.clusters/create
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def cloud_platform_cluster_body(**options)
diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb
index 620fa37d455..dbbd4ad4d40 100644
--- a/spec/support/prometheus/additional_metrics_shared_examples.rb
+++ b/spec/support/prometheus/additional_metrics_shared_examples.rb
@@ -41,16 +41,30 @@ RSpec.shared_examples 'additional metrics query' do
end
describe 'project has Kubernetes service' do
- let(:project) { create(:kubernetes_project) }
- let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
- let(:kube_namespace) { project.kubernetes_service.actual_namespace }
+ shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
+ let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
+ let(:kube_namespace) { project.deployment_platform.actual_namespace }
- it_behaves_like 'query context containing environment slug and filter'
+ it_behaves_like 'query context containing environment slug and filter'
- it 'query context contains kube_namespace' do
- expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
+ it 'query context contains kube_namespace' do
+ expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace))
- subject.query(*query_params)
+ subject.query(*query_params)
+ end
+ end
+
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end
end
diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb
index 369775db462..8cf8f45a8b2 100644
--- a/spec/support/query_recorder.rb
+++ b/spec/support/query_recorder.rb
@@ -41,7 +41,8 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations
match do |block|
- query_count(&block) > expected_count + threshold
+ @subject_block = block
+ actual_count > expected_count + threshold
end
failure_message_when_negated do |actual|
@@ -55,6 +56,11 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
self
end
+ def for_query(query)
+ @query = query
+ self
+ end
+
def threshold
@threshold.to_i
end
@@ -68,12 +74,15 @@ RSpec::Matchers.define :exceed_query_limit do |expected|
end
def actual_count
- @recorder.count
+ @actual_count ||= if @query
+ recorder.log.select { |recorded| recorded =~ @query }.size
+ else
+ recorder.count
+ end
end
- def query_count(&block)
- @recorder = ActiveRecord::QueryRecorder.new(&block)
- @recorder.count
+ def recorder
+ @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block)
end
def count_queries(queries)
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
new file mode 100644
index 00000000000..935c08221e0
--- /dev/null
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -0,0 +1,240 @@
+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') }
+
+ describe "POST #create" do
+ context 'when a user is not authorized to upload a file' do
+ it 'returns 404 status' do
+ post :create, params.merge(file: jpg, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when a user can upload a file' do
+ before do
+ sign_in(user)
+ model.add_developer(user)
+ end
+
+ context "without params['file']" do
+ it "returns an error" do
+ post :create, params.merge(format: :json)
+
+ expect(response).to have_gitlab_http_status(422)
+ end
+ end
+
+ context 'with valid image' do
+ before do
+ post :create, params.merge(file: jpg, format: :json)
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+
+ # NOTE: This is as close as we're getting to an Integration test for this
+ # behavior. We're avoiding a proper Feature test because those should be
+ # testing things entirely user-facing, which the Upload model is very much
+ # not.
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq(model)
+ end
+ end
+ end
+
+ context 'with valid non-image file' do
+ before do
+ post :create, params.merge(file: txt, format: :json)
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+ end
+ end
+ end
+
+ describe "GET #show" do
+ let(:show_upload) do
+ get :show, params.merge(secret: "123456", filename: "image.jpg")
+ end
+
+ context "when the model is public" do
+ before do
+ model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ 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
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ 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
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
+
+ context "when the model is private" do
+ before do
+ model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ 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)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "redirects to the sign in page" do
+ show_upload
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "redirects to the sign in page" do
+ show_upload
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the user has access to the project" do
+ before do
+ model.add_developer(user)
+ 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
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ 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)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb
new file mode 100644
index 00000000000..4a25bb9b750
--- /dev/null
+++ b/spec/support/shared_examples/throttled_touch.rb
@@ -0,0 +1,20 @@
+shared_examples_for 'throttled touch' do
+ describe '#touch' do
+ it 'updates the updated_at timestamp' do
+ Timecop.freeze do
+ subject.touch
+ expect(subject.updated_at).to eq(Time.zone.now)
+ end
+ end
+
+ it 'updates the object at most once per minute' do
+ first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2)
+ second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5)
+
+ Timecop.freeze(first_updated_at) { subject.touch }
+ Timecop.freeze(second_updated_at) { subject.touch }
+
+ expect(subject.updated_at).to eq(first_updated_at)
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index f3deae0f455..f9121cce985 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -12,6 +12,25 @@ RSpec.configure do |config|
raise GRPC::Unavailable.new('Gitaly broken in this spec')
end
- Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ # 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
+ cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}"
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hset(cache_key, :first_failure, first_failure.to_i)
+ redis.hset(cache_key, :last_failure, last_failure.to_i)
+ redis.hset(cache_key, :failure_count, failure_count.to_i)
+ end
+ end
+ end
+
+ config.after(:each, :broken_storage) do
+ Gitlab::Git::Storage.redis.with(&:flushall)
end
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 4ead78529c3..b36cf3c544c 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -43,6 +43,8 @@ module StubConfiguration
end
def stub_storage_settings(messages)
+ messages.deep_stringify_keys!
+
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
messages.each do |storage_name, storage_settings|
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 5f22d886910..c1618f5086c 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -21,6 +21,12 @@ module StubGitlabCalls
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml }
end
+ def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml')
+ allow_any_instance_of(Repository)
+ .to receive(:gitlab_ci_yml_for).with(sha, path)
+ .and_return(gitlab_ci_yaml)
+ end
+
def stub_ci_builds_disabled
allow_any_instance_of(Project).to receive(:builds_enabled?).and_return(false)
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index fff120fcb88..b300b493f86 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -120,6 +120,7 @@ module TestEnv
FileUtils.mkdir_p(repos_path)
FileUtils.mkdir_p(backup_path)
FileUtils.mkdir_p(pages_path)
+ FileUtils.mkdir_p(artifacts_path)
end
def clean_gitlab_test_path
@@ -233,6 +234,10 @@ module TestEnv
Gitlab.config.pages.path
end
+ def artifacts_path
+ Gitlab.config.artifacts.path
+ end
+
# When no cached assets exist, manually hit the root path to create them
#
# Otherwise they'd be created by the first test, often timing out and
diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb
new file mode 100644
index 00000000000..d05eda08201
--- /dev/null
+++ b/spec/support/track_untracked_uploads_helpers.rb
@@ -0,0 +1,20 @@
+module TrackUntrackedUploadsHelpers
+ def uploaded_file
+ fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ fixture_file_upload(fixture_path)
+ end
+
+ def ensure_temporary_tracking_table_exists
+ Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
+ end
+
+ def drop_temp_table_if_exists
+ ActiveRecord::Base.connection.drop_table(:untracked_files_for_uploads) if ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)
+ end
+
+ def create_or_update_appearance(attrs)
+ a = Appearance.first_or_initialize(title: 'foo', description: 'bar')
+ a.update!(attrs)
+ a
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bf2e11bc360..b41c3b3958a 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -212,7 +212,7 @@ describe 'gitlab:app namespace rake task' do
# Avoid asking gitaly about the root ref (which will fail beacuse of the
# mocked storages)
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Repository).to receive(:empty?).and_return(false)
end
after do
diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb
deleted file mode 100644
index 2a3bd0e3bb2..00000000000
--- a/spec/uploaders/artifact_uploader_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'rails_helper'
-
-describe ArtifactUploader do
- let(:job) { create(:ci_build) }
- let(:uploader) { described_class.new(job, :artifacts_file) }
- let(:path) { Gitlab.config.artifacts.path }
-
- describe '.local_artifacts_store' do
- subject { described_class.local_artifacts_store }
-
- 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 }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with('tmp/uploads/') }
- end
-
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with("#{job.project_id}/#{job.id}") }
- 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
-
- describe '#filename' do
- # we need to use uploader, as this makes to use mounter
- # which initialises uploader.file object
- let(:uploader) { job.artifacts_file }
-
- subject { uploader.filename }
-
- it { is_expected.to be_nil }
-
- context 'with artifacts' do
- let(:job) { create(:ci_build, :artifacts) }
-
- it { is_expected.not_to be_nil }
- end
- end
-end
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
new file mode 100644
index 00000000000..14fd5f3600f
--- /dev/null
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -0,0 +1,51 @@
+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 }
+
+ 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
+
+ context 'file is stored in valid local_path' do
+ let(:file) do
+ fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ end
+
+ before do
+ uploader.store!(file)
+ end
+
+ subject { uploader.file.path }
+
+ it { is_expected.to start_with(local_path) }
+ 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 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
new file mode 100644
index 00000000000..efeffb78772
--- /dev/null
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -0,0 +1,77 @@
+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 }
+
+ describe '.local_store_path' do
+ subject { described_class.local_store_path }
+
+ 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 }
+
+ 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
+ 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
+
+ describe '#filename' do
+ # we need to use uploader, as this makes to use mounter
+ # which initialises uploader.file object
+ let(:uploader) { job.artifacts_file }
+
+ subject { uploader.filename }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'file is stored in valid path' do
+ let(:file) do
+ fixture_file_upload(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ end
+
+ before do
+ uploader.store!(file)
+ end
+
+ subject { uploader.file.path }
+
+ it { is_expected.to start_with(local_path) }
+ it { is_expected.to include("/#{job.created_at.utc.strftime('%Y_%m')}/") }
+ it { is_expected.to include("/#{job.project_id}/") }
+ it { is_expected.to end_with("ci_build_artifacts.zip") }
+ end
+end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
new file mode 100644
index 00000000000..c6c4500c179
--- /dev/null
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe NamespaceFileUploader do
+ let(:group) { build_stubbed(:group) }
+ let(:uploader) { described_class.new(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)
+
+ expect(described_class.absolute_path(upload_record))
+ .to include("-/system/namespace/#{group.id}")
+ end
+ end
+end
diff --git a/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb
new file mode 100644
index 00000000000..2f58eec86dc
--- /dev/null
+++ b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'dashboard/projects/_blank_state_admin_welcome.html.haml' do
+ let(:user) { create(:admin) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ it 'links to new group path' do
+ render
+
+ expect(rendered).to have_link('Create a group', href: new_group_path)
+ end
+end
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index 32c95c6bb0d..a9c32122600 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -2,14 +2,15 @@ require 'spec_helper'
describe 'projects/commit/show.html.haml' do
let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
before do
assign(:project, project)
assign(:repository, project.repository)
- assign(:commit, project.commit)
- assign(:noteable, project.commit)
+ assign(:commit, commit)
+ assign(:noteable, commit)
assign(:notes, [])
- assign(:diffs, project.commit.diffs)
+ assign(:diffs, commit.diffs)
allow(view).to receive(:current_user).and_return(nil)
allow(view).to receive(:can?).and_return(false)
@@ -43,4 +44,19 @@ describe 'projects/commit/show.html.haml' do
expect(rendered).not_to have_selector('.limit-container-width')
end
end
+
+ context 'in the context of a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ assign(:merge_request, merge_request)
+ render
+ end
+
+ it 'shows that it is in the context of a merge request' do
+ merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id)
+ expect(rendered).to have_content("This commit is part of merge request")
+ expect(rendered).to have_link(merge_request.to_reference, merge_request_url)
+ end
+ end
end
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index efed2e02a1b..3ca67114558 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -25,8 +25,8 @@ describe 'projects/merge_requests/_commits.html.haml' do
it 'shows commits from source project' do
render
- commit = source_project.commit(merge_request.source_branch)
- href = project_commit_path(source_project, commit)
+ commit = merge_request.commits.first # HEAD
+ href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit)
expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href)
end
diff --git a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
new file mode 100644
index 00000000000..e7c40421f1f
--- /dev/null
+++ b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/diffs/_diffs.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: user) }
+
+ before do
+ allow(view).to receive(:url_for).and_return(controller.request.fullpath)
+
+ assign(:merge_request, merge_request)
+ assign(:environment, merge_request.environments_for(user).last)
+ assign(:diffs, merge_request.diffs)
+ assign(:merge_request_diffs, merge_request.diffs)
+ assign(:diff_notes_disabled, true) # disable note creation
+ assign(:use_legacy_diff_notes, false)
+ assign(:grouped_diff_discussions, {})
+ assign(:notes, [])
+ end
+
+ context 'for a commit' do
+ let(:commit) { merge_request.commits.last }
+
+ before do
+ assign(:commit, commit)
+ end
+
+ it "shows the commit scope" do
+ render
+
+ expect(rendered).to have_content "Only comments from the following commit are shown below"
+ 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 c757ccf02d3..95f0be49412 100644
--- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -35,7 +35,7 @@ describe 'projects/pipelines_settings/_show' do
context 'when kubernetes is active' do
before do
- project.build_kubernetes_service(active: true)
+ create(:kubernetes_service, project: project)
end
context 'when auto devops domain is not defined' do
diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_blob_item.html.haml_spec.rb
new file mode 100644
index 00000000000..6a477c712ff
--- /dev/null
+++ b/spec/views/projects/tree/_blob_item.html.haml_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe 'projects/tree/_blob_item' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
+
+ before do
+ assign(:project, project)
+ assign(:repository, repository)
+ assign(:id, File.join('master', ''))
+ assign(:lfs_blob_ids, [])
+ end
+
+ it 'renders blob item' do
+ render_partial(blob_item)
+
+ expect(rendered).to have_content(blob_item.name)
+ expect(rendered).not_to have_selector('.label-lfs', text: 'LFS')
+ end
+
+ describe 'LFS blob' do
+ before do
+ assign(:lfs_blob_ids, [blob_item].map(&:id))
+
+ render_partial(blob_item)
+ end
+
+ it 'renders LFS badge' do
+ expect(rendered).to have_selector('.label-lfs', text: 'LFS')
+ end
+ end
+
+ def render_partial(blob_item)
+ render partial: 'projects/tree/blob_item', locals: {
+ blob_item: blob_item,
+ type: 'blob'
+ }
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 3c25e341b39..44b32df0395 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -9,6 +9,7 @@ describe 'projects/tree/show' do
before do
assign(:project, project)
assign(:repository, repository)
+ assign(:lfs_blob_ids, [])
allow(view).to receive(:can?).and_return(true)
allow(view).to receive(:can_collaborate_with_project?).and_return(true)
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 90ed1309d4a..0d6eb536c33 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -65,7 +65,6 @@ describe AuthorizedProjectsWorker do
args_list = build_args_list(project.owner.id)
push_bulk_args = {
'class' => described_class,
- 'queue' => described_class.sidekiq_options['queue'],
'args' => args_list
}
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index 4f6e3474634..1c54cf55fa0 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -10,35 +10,4 @@ describe BackgroundMigrationWorker, :sidekiq do
described_class.new.perform('Foo', [10, 20])
end
end
-
- describe '.perform_bulk' do
- it 'enqueues background migrations in bulk' do
- Sidekiq::Testing.fake! do
- described_class.perform_bulk([['Foo', [1]], ['Foo', [2]]])
-
- expect(described_class.jobs.count).to eq 2
- expect(described_class.jobs).to all(include('enqueued_at'))
- end
- end
- end
-
- describe '.perform_bulk_in' do
- context 'when delay is valid' do
- it 'correctly schedules background migrations' do
- Sidekiq::Testing.fake! do
- described_class.perform_bulk_in(1.minute, [['Foo', [1]], ['Foo', [2]]])
-
- expect(described_class.jobs.count).to eq 2
- expect(described_class.jobs).to all(include('at'))
- end
- end
- end
-
- context 'when delay is invalid' do
- it 'raises an ArgumentError exception' do
- expect { described_class.perform_bulk_in(-60, [['Foo']]) }
- .to raise_error(ArgumentError)
- end
- end
- end
end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
new file mode 100644
index 00000000000..901d77178bc
--- /dev/null
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe ApplicationWorker do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'Gitlab::Foo::Bar::DummyWorker'
+ end
+
+ include ApplicationWorker
+ end
+ end
+
+ describe 'Sidekiq options' do
+ it 'sets the queue name based on the class name' do
+ expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
+ end
+ end
+
+ describe '.queue_namespace' do
+ it 'sets the queue name based on the class name' do
+ worker.queue_namespace :some_namespace
+
+ expect(worker.queue).to eq('some_namespace:foo_bar_dummy')
+ end
+ end
+
+ describe '.queue' do
+ it 'returns the queue name' do
+ worker.sidekiq_options queue: :some_queue
+
+ expect(worker.queue).to eq('some_queue')
+ end
+ end
+
+ describe '.bulk_perform_async' do
+ it 'enqueues jobs in bulk' do
+ Sidekiq::Testing.fake! do
+ worker.bulk_perform_async([['Foo', [1]], ['Foo', [2]]])
+
+ expect(worker.jobs.count).to eq 2
+ expect(worker.jobs).to all(include('enqueued_at'))
+ end
+ end
+ end
+
+ describe '.bulk_perform_in' do
+ context 'when delay is valid' do
+ it 'correctly schedules jobs' do
+ Sidekiq::Testing.fake! do
+ worker.bulk_perform_in(1.minute, [['Foo', [1]], ['Foo', [2]]])
+
+ expect(worker.jobs.count).to eq 2
+ expect(worker.jobs).to all(include('at'))
+ end
+ end
+ end
+
+ context 'when delay is invalid' do
+ it 'raises an ArgumentError exception' do
+ expect { worker.bulk_perform_in(-60, [['Foo']]) }
+ .to raise_error(ArgumentError)
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
index 1050651fa51..4118b9aa194 100644
--- a/spec/workers/concerns/cluster_queue_spec.rb
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe ClusterQueue do
let(:worker) do
Class.new do
- include Sidekiq::Worker
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
include ClusterQueue
end
end
it 'sets a default pipelines queue automatically' do
expect(worker.sidekiq_options['queue'])
- .to eq :gcp_cluster
+ .to eq 'gcp_cluster:dummy'
end
end
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
index 5d1336c21a6..c042a52f41f 100644
--- a/spec/workers/concerns/cronjob_queue_spec.rb
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe CronjobQueue do
let(:worker) do
Class.new do
- include Sidekiq::Worker
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
include CronjobQueue
end
end
it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
+ expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy')
end
it 'disables retrying of failed jobs' do
diff --git a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb b/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
deleted file mode 100644
index 512baec8b7e..00000000000
--- a/spec/workers/concerns/dedicated_sidekiq_queue_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe DedicatedSidekiqQueue do
- let(:worker) do
- Class.new do
- def self.name
- 'Foo::Bar::DummyWorker'
- end
-
- include Sidekiq::Worker
- include DedicatedSidekiqQueue
- end
- end
-
- describe 'queue names' do
- it 'sets the queue name based on the class name' do
- expect(worker.sidekiq_options['queue']).to eq('foo_bar_dummy')
- end
- end
-end
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 3ccf06f2d7d..68cfe9d5545 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -3,6 +3,10 @@ require 'spec_helper'
describe Gitlab::GithubImport::ObjectImporter do
let(:worker) do
Class.new do
+ def self.name
+ 'DummyWorker'
+ end
+
include(Gitlab::GithubImport::ObjectImporter)
def counter_name
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
index 321ae3fe978..a96f583aff7 100644
--- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
@@ -3,10 +3,14 @@ require 'spec_helper'
describe Gitlab::GithubImport::Queue do
it 'sets the Sidekiq options for the worker' do
worker = Class.new do
- include Sidekiq::Worker
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
include Gitlab::GithubImport::Queue
end
- expect(worker.sidekiq_options['queue']).to eq('github_importer')
+ expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy')
end
end
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
index eac5a770e5f..a312b307fce 100644
--- a/spec/workers/concerns/pipeline_queue_spec.rb
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -3,22 +3,17 @@ require 'spec_helper'
describe PipelineQueue do
let(:worker) do
Class.new do
- include Sidekiq::Worker
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
include PipelineQueue
end
end
it 'sets a default pipelines queue automatically' do
expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_default'
- end
-
- describe '.enqueue_in' do
- it 'sets a custom sidekiq queue with prefix and group' do
- worker.enqueue_in(group: :processing)
-
- expect(worker.sidekiq_options['queue'])
- .to eq 'pipeline_processing'
- end
+ .to eq 'pipeline_default:dummy'
end
end
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
index 8868e969829..d2eeecfc9a8 100644
--- a/spec/workers/concerns/repository_check_queue_spec.rb
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe RepositoryCheckQueue do
let(:worker) do
Class.new do
- include Sidekiq::Worker
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
include RepositoryCheckQueue
end
end
it 'sets the queue name of a worker' do
- expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check')
+ expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy')
end
it 'disables retrying of failed jobs' do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index 30908534eb3..9e3b99b3502 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -1,44 +1,36 @@
require 'spec_helper'
describe 'Every Sidekiq worker' do
- let(:workers) do
- root = Rails.root.join('app', 'workers')
- concerns = root.join('concerns').to_s
-
- workers = Dir[root.join('**', '*.rb')]
- .reject { |path| path.start_with?(concerns) }
-
- workers.map do |path|
- ns = Pathname.new(path).relative_path_from(root).to_s.gsub('.rb', '')
-
- ns.camelize.constantize
- end
- end
-
it 'does not use the default queue' do
- workers.each do |worker|
- expect(worker.sidekiq_options['queue'].to_s).not_to eq('default')
- end
+ expect(Gitlab::SidekiqConfig.workers.map(&:queue)).not_to include('default')
end
it 'uses the cronjob queue when the worker runs as a cronjob' do
- cron_workers = Settings.cron_jobs
- .map { |job_name, options| options['job_class'].constantize }
- .to_set
+ expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(start_with('cronjob:'))
+ end
- workers.each do |worker|
- next unless cron_workers.include?(worker)
+ it 'has its queue in app/workers/all_queues.yml', :aggregate_failures do
+ file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set
- expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob')
- end
+ worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set
+ worker_queues << ActionMailer::DeliveryJob.queue_name
+ worker_queues << 'default'
+
+ missing_from_file = worker_queues - file_worker_queues
+ expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in app/workers/all_queues.yml"
+
+ unncessarily_in_file = file_worker_queues - worker_queues
+ expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in app/workers/all_queues.yml"
end
- it 'defines the queue in the Sidekiq configuration file' do
- config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
- queue_names = config[:queues].map { |(queue, _)| queue }.to_set
+ it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do
+ config_queues = Gitlab::SidekiqConfig.config_queues.to_set
+
+ Gitlab::SidekiqConfig.workers.each do |worker|
+ queue = worker.queue
+ queue_namespace = queue.split(':').first
- workers.each do |worker|
- expect(queue_names).to include(worker.sidekiq_options['queue'].to_s)
+ expect(config_queues).to include(queue).or(include(queue_namespace))
end
end
end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index bed5c5e2ecb..e1a56c72162 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -11,12 +11,8 @@ describe ExpireBuildInstanceArtifactsWorker do
end
context 'with expired artifacts' do
- let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } }
-
context 'when associated project is valid' do
- let(:build) do
- create(:ci_build, :artifacts, artifacts_expiry)
- end
+ let(:build) { create(:ci_build, :artifacts, :expired) }
it 'does expire' do
expect(build.reload.artifacts_expired?).to be_truthy
@@ -26,14 +22,14 @@ describe ExpireBuildInstanceArtifactsWorker do
expect(build.reload.artifacts_file.exists?).to be_falsey
end
- it 'does nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).to be_nil
+ it 'does remove the job artifact record' do
+ expect(build.reload.job_artifacts_archive).to be_nil
end
end
end
context 'with not yet expired artifacts' do
- let(:build) do
+ set(:build) do
create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days)
end
@@ -45,8 +41,8 @@ describe ExpireBuildInstanceArtifactsWorker do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not remove the job artifact record' do
+ expect(build.reload.job_artifacts_archive).not_to be_nil
end
end
@@ -61,13 +57,13 @@ describe ExpireBuildInstanceArtifactsWorker do
expect(build.reload.artifacts_file.exists?).to be_truthy
end
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not remove the job artifact record' do
+ expect(build.reload.job_artifacts_archive).not_to be_nil
end
end
context 'for expired artifacts' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+ let(:build) { create(:ci_build, :expired) }
it 'is still expired' do
expect(build.reload.artifacts_expired?).to be_truthy
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 05eecf5f0bb..5d9b0679796 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -66,19 +66,21 @@ describe PostReceive do
end
context "gitlab-ci.yml" do
+ let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
+
subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
context "creates a Ci::Pipeline for every change" do
before do
stub_ci_pipeline_to_return_yaml_file
- # TODO, don't stub private methods
- #
- allow_any_instance_of(Ci::CreatePipelineService)
- .to receive(:commit).and_return(OpenStruct.new(id: '123456'))
+ allow_any_instance_of(Project)
+ .to receive(:commit)
+ .and_return(project.commit)
allow_any_instance_of(Repository)
- .to receive(:branch_exists?).and_return(true)
+ .to receive(:branch_exists?)
+ .and_return(true)
end
it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 5f4453c15d6..3da851de067 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -1,15 +1,28 @@
require 'spec_helper'
describe ReactiveCachingWorker do
- let(:project) { create(:kubernetes_project) }
- let(:service) { project.deployment_service }
- subject { described_class.new.perform("KubernetesService", service.id) }
+ let(:service) { project.deployment_platform }
describe '#perform' do
- it 'calls #exclusively_update_reactive_cache!' do
- expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+ context 'when user configured kubernetes from Integration > Kubernetes' do
+ let(:project) { create(:kubernetes_project) }
- subject
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(KubernetesService).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("KubernetesService", service.id)
+ end
+ end
+
+ context 'when user configured kubernetes from CI/CD > Clusters' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ it 'calls #exclusively_update_reactive_cache!' do
+ expect_any_instance_of(Clusters::Platforms::Kubernetes).to receive(:exclusively_update_reactive_cache!)
+
+ described_class.new.perform("Clusters::Platforms::Kubernetes", service.id)
+ end
end
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index e881ec37ae5..74c85848b7e 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe RepositoryForkWorker do
- let(:project) { create(:project, :repository, :import_scheduled) }
- let(:fork_project) { create(:project, :repository, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
let(:shell) { Gitlab::Shell.new }
subject { described_class.new }
@@ -12,50 +12,39 @@ describe RepositoryForkWorker do
end
describe "#perform" do
+ def perform!
+ subject.perform(fork_project.id, '/test/path', project.disk_path)
+ end
+
+ def expect_fork_repository
+ expect(shell).to receive(:fork_repository).with(
+ '/test/path',
+ project.disk_path,
+ fork_project.repository_storage_path,
+ fork_project.disk_path
+ )
+ end
+
describe 'when a worker was reset without cleanup' do
let(:jid) { '12345678' }
- let(:started_project) { create(:project, :repository, :import_started) }
it 'creates a new repository from a fork' do
allow(subject).to receive(:jid).and_return(jid)
- expect(shell).to receive(:fork_repository).with(
- '/test/path',
- project.full_path,
- project.repository_storage_path,
- fork_project.namespace.full_path
- ).and_return(true)
-
- subject.perform(
- project.id,
- '/test/path',
- project.full_path,
- fork_project.namespace.full_path)
+ expect_fork_repository.and_return(true)
+
+ perform!
end
end
it "creates a new repository from a fork" do
- expect(shell).to receive(:fork_repository).with(
- '/test/path',
- project.full_path,
- project.repository_storage_path,
- fork_project.namespace.full_path
- ).and_return(true)
+ expect_fork_repository.and_return(true)
- subject.perform(
- project.id,
- '/test/path',
- project.full_path,
- fork_project.namespace.full_path)
+ perform!
end
it 'flushes various caches' do
- expect(shell).to receive(:fork_repository).with(
- '/test/path',
- project.full_path,
- project.repository_storage_path,
- fork_project.namespace.full_path
- ).and_return(true)
+ expect_fork_repository.and_return(true)
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
.and_call_original
@@ -63,32 +52,22 @@ describe RepositoryForkWorker do
expect_any_instance_of(Repository).to receive(:expire_exists_cache)
.and_call_original
- subject.perform(project.id, '/test/path', project.full_path,
- fork_project.namespace.full_path)
+ perform!
end
it "handles bad fork" do
- source_path = project.full_path
- target_path = fork_project.namespace.full_path
- error_message = "Unable to fork project #{project.id} for repository #{source_path} -> #{target_path}"
+ error_message = "Unable to fork project #{fork_project.id} for repository #{project.full_path} -> #{fork_project.full_path}"
- expect(shell).to receive(:fork_repository).and_return(false)
+ expect_fork_repository.and_return(false)
- expect do
- subject.perform(project.id, '/test/path', source_path, target_path)
- end.to raise_error(RepositoryForkWorker::ForkError, error_message)
+ expect { perform! }.to raise_error(RepositoryForkWorker::ForkError, error_message)
end
it 'handles unexpected error' do
- source_path = project.full_path
- target_path = fork_project.namespace.full_path
-
- allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_raise(RuntimeError)
+ expect_fork_repository.and_raise(RuntimeError)
- expect do
- subject.perform(project.id, '/test/path', source_path, target_path)
- end.to raise_error(RepositoryForkWorker::ForkError)
- expect(project.reload.import_status).to eq('failed')
+ expect { perform! }.to raise_error(RepositoryForkWorker::ForkError)
+ expect(fork_project.reload.import_status).to eq('failed')
end
end
end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index f8b55e873df..c2c2a5f9121 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -14,7 +14,6 @@ describe StuckMergeJobsWorker do
mr_with_sha.reload
mr_without_sha.reload
-
expect(mr_with_sha).to be_merged
expect(mr_without_sha).to be_opened
expect(mr_with_sha.merge_jid).to be_present
@@ -24,10 +23,13 @@ describe StuckMergeJobsWorker do
it 'updates merge request to opened when locked but has not been merged' do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
+ pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha)
worker.perform
- expect(merge_request.reload).to be_opened
+ merge_request.reload
+ expect(merge_request).to be_opened
+ expect(merge_request.head_pipeline).to eq(pipeline)
end
it 'logs updated stuck merge job ids' do
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
new file mode 100644
index 00000000000..9adde5fc21a
--- /dev/null
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UpdateHeadPipelineForMergeRequestWorker do
+ describe '#perform' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ context 'when pipeline exists for the source project and branch' do
+ before do
+ create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha)
+ end
+
+ it 'updates the head_pipeline_id of the merge_request' do
+ expect { subject.perform(merge_request.id) }.to change { merge_request.reload.head_pipeline_id }
+ end
+
+ context 'when merge request sha does not equal pipeline sha' do
+ before do
+ merge_request.merge_request_diff.update(head_commit_sha: 'different_sha')
+ end
+
+ it 'does not update head_pipeline_id' do
+ expect { subject.perform(merge_request.id) }.not_to raise_error
+
+ expect(merge_request.reload.head_pipeline_id).to eq(nil)
+ end
+ end
+ end
+
+ context 'when pipeline does not exist for the source project and branch' do
+ it 'does not update the head_pipeline_id of the merge_request' do
+ expect { subject.perform(merge_request.id) }.not_to change { merge_request.reload.head_pipeline_id }
+ end
+ end
+ end
+end