summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/config/application_spec.rb34
-rw-r--r--spec/controllers/admin/appearances_controller_spec.rb55
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb19
-rw-r--r--spec/controllers/admin/users_controller_spec.rb25
-rw-r--r--spec/controllers/application_controller_spec.rb8
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb4
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb9
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb60
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb2
-rw-r--r--spec/controllers/groups/shared_projects_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb8
-rw-r--r--spec/controllers/import/github_controller_spec.rb4
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb3
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb69
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb10
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb16
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb37
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb13
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb20
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb12
-rw-r--r--spec/controllers/snippets_controller_spec.rb4
-rw-r--r--spec/factories/import_states.rb (renamed from spec/factories/import_state.rb)0
-rw-r--r--spec/factories/personal_access_tokens.rb5
-rw-r--r--spec/factories/projects.rb14
-rw-r--r--spec/factories/users.rb11
-rw-r--r--spec/features/admin/admin_appearance_spec.rb32
-rw-r--r--spec/features/dashboard/projects_spec.rb11
-rw-r--r--spec/features/dashboard/shortcuts_spec.rb2
-rw-r--r--spec/features/dashboard/snippets_spec.rb15
-rw-r--r--spec/features/display_system_header_and_footer_bar_spec.rb137
-rw-r--r--spec/features/groups/labels/create_spec.rb23
-rw-r--r--spec/features/groups/labels/index_spec.rb14
-rw-r--r--spec/features/issuables/issuable_list_spec.rb24
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb11
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb21
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb5
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb6
-rw-r--r--spec/features/profiles/active_sessions_spec.rb48
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb24
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb5
-rw-r--r--spec/features/projects/blobs/edit_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb32
-rw-r--r--spec/features/projects/members/invite_group_spec.rb2
-rw-r--r--spec/features/projects/pages_spec.rb107
-rw-r--r--spec/features/projects/services/user_activates_issue_tracker_spec.rb35
-rw-r--r--spec/features/projects/services/user_activates_youtrack_spec.rb89
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/user_manages_group_links_spec.rb1
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb7
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb7
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb7
-rw-r--r--spec/features/projects_spec.rb4
-rw-r--r--spec/features/security/group/private_access_spec.rb32
-rw-r--r--spec/features/users/signup_spec.rb28
-rw-r--r--spec/finders/concerns/finder_methods_spec.rb22
-rw-r--r--spec/finders/issues_finder_spec.rb58
-rw-r--r--spec/finders/merge_requests_finder_spec.rb26
-rw-r--r--spec/fixtures/api/schemas/entities/diff_viewer.json11
-rw-r--r--spec/fixtures/api/schemas/environment.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/milestone.json1
-rw-r--r--spec/fixtures/security-reports/master/gl-container-scanning-report.json23
-rw-r--r--spec/frontend/__mocks__/file_mock.js1
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js (renamed from spec/javascripts/gfm_auto_complete_spec.js)85
-rw-r--r--spec/frontend/issuable_suggestions/components/app_spec.js (renamed from spec/javascripts/issuable_suggestions/components/app_spec.js)0
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js (renamed from spec/javascripts/issuable_suggestions/components/item_spec.js)0
-rw-r--r--spec/frontend/issuable_suggestions/mock_data.js (renamed from spec/javascripts/issuable_suggestions/mock_data.js)0
-rw-r--r--spec/frontend/lib/utils/ajax_cache_spec.js (renamed from spec/javascripts/lib/utils/ajax_cache_spec.js)85
-rw-r--r--spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap20
-rw-r--r--spec/frontend/notes/components/discussion_jump_to_next_button_spec.js30
-rw-r--r--spec/frontend/test_setup.js18
-rw-r--r--spec/graphql/features/authorization_spec.rb106
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb31
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb71
-rw-r--r--spec/graphql/resolvers/merge_requests_resolver_spec.rb (renamed from spec/graphql/resolvers/merge_request_resolver_spec.rb)34
-rw-r--r--spec/graphql/types/issuable_state_enum_spec.rb9
-rw-r--r--spec/graphql/types/issue_state_enum_spec.rb9
-rw-r--r--spec/graphql/types/merge_request_state_enum_spec.rb13
-rw-r--r--spec/graphql/types/project_type_spec.rb6
-rw-r--r--spec/helpers/appearances_helper_spec.rb76
-rw-r--r--spec/helpers/import_helper_spec.rb57
-rw-r--r--spec/helpers/labels_helper_spec.rb13
-rw-r--r--spec/helpers/preferences_helper_spec.rb7
-rw-r--r--spec/javascripts/diffs/components/app_spec.js26
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js26
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js20
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js35
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js12
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js8
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js2
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js19
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js4
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js10
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js220
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js11
-rw-r--r--spec/javascripts/fixtures/blob.rb1
-rw-r--r--spec/javascripts/fixtures/commit.rb1
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb2
-rw-r--r--spec/javascripts/fixtures/groups.rb2
-rw-r--r--spec/javascripts/fixtures/issues.rb2
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb7
-rw-r--r--spec/javascripts/fixtures/projects.rb12
-rw-r--r--spec/javascripts/fixtures/snippet.rb4
-rw-r--r--spec/javascripts/fixtures/u2f.rb3
-rw-r--r--spec/javascripts/helpers/vuex_action_helper.js5
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js9
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js186
-rw-r--r--spec/javascripts/import_projects/components/imported_project_table_row_spec.js50
-rw-r--r--spec/javascripts/import_projects/components/provider_repo_table_row_spec.js91
-rw-r--r--spec/javascripts/import_projects/store/actions_spec.js284
-rw-r--r--spec/javascripts/import_projects/store/getters_spec.js83
-rw-r--r--spec/javascripts/import_projects/store/mutations_spec.js34
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js8
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js32
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js4
-rw-r--r--spec/javascripts/notes/components/discussion_filter_note_spec.js93
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js22
-rw-r--r--spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js33
-rw-r--r--spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js31
-rw-r--r--spec/javascripts/notes/components/note_actions/reply_button_spec.js22
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js17
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js39
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js152
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js25
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js81
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/changed_file_icon_spec.js21
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/panel_resizer_spec.js9
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb155
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb36
-rw-r--r--spec/lib/banzai/filter/footnote_filter_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/client_spec.rb4
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb4
-rw-r--r--spec/lib/feature_spec.rb5
-rw-r--r--spec/lib/gitlab/background_migration/digest_column_spec.rb4
-rw-r--r--spec/lib/gitlab/chat/command_spec.rb77
-rw-r--r--spec/lib/gitlab/chat/output_spec.rb101
-rw-r--r--spec/lib/gitlab/chat/responder/base_spec.rb48
-rw-r--r--spec/lib/gitlab/chat/responder/slack_spec.rb77
-rw-r--r--spec/lib/gitlab/chat/responder_spec.rb32
-rw-r--r--spec/lib/gitlab/chat_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb33
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb22
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb9
-rw-r--r--spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb18
-rw-r--r--spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb42
-rw-r--r--spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb5
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb31
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb18
-rw-r--r--spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/authorize_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/project.json20
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/import_export/shared_spec.rb10
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb30
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb36
-rw-r--r--spec/lib/gitlab/profiler_spec.rb8
-rw-r--r--spec/lib/gitlab/project_template_spec.rb8
-rw-r--r--spec/lib/gitlab/serializer/pagination_spec.rb10
-rw-r--r--spec/lib/gitlab/slash_commands/application_help_spec.rb19
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/error_spec.rb15
-rw-r--r--spec/lib/gitlab/slash_commands/presenters/run_spec.rb79
-rw-r--r--spec/lib/gitlab/slash_commands/run_spec.rb123
-rw-r--r--spec/lib/gitlab/tracing_spec.rb15
-rw-r--r--spec/lib/sentry/client_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb56
-rw-r--r--spec/models/active_session_spec.rb5
-rw-r--r--spec/models/appearance_spec.rb15
-rw-r--r--spec/models/ci/build_spec.rb1
-rw-r--r--spec/models/ci/pipeline_spec.rb228
-rw-r--r--spec/models/clusters/applications/helm_spec.rb7
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb12
-rw-r--r--spec/models/clusters/applications/knative_spec.rb24
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb73
-rw-r--r--spec/models/clusters/applications/runner_spec.rb16
-rw-r--r--spec/models/clusters/cluster_spec.rb6
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb16
-rw-r--r--spec/models/commit_collection_spec.rb14
-rw-r--r--spec/models/concerns/milestoneish_spec.rb47
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb12
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb235
-rw-r--r--spec/models/environment_spec.rb22
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb18
-rw-r--r--spec/models/issue_spec.rb9
-rw-r--r--spec/models/merge_request_diff_spec.rb14
-rw-r--r--spec/models/merge_request_spec.rb20
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb61
-rw-r--r--spec/models/project_services/slack_slash_commands_service_spec.rb7
-rw-r--r--spec/models/project_services/youtrack_service_spec.rb38
-rw-r--r--spec/models/project_spec.rb36
-rw-r--r--spec/models/prometheus_metric_spec.rb13
-rw-r--r--spec/models/repository_spec.rb23
-rw-r--r--spec/models/user_spec.rb29
-rw-r--r--spec/policies/board_policy_spec.rb67
-rw-r--r--spec/policies/commit_policy_spec.rb53
-rw-r--r--spec/policies/group_policy_spec.rb40
-rw-r--r--spec/policies/note_policy_spec.rb94
-rw-r--r--spec/policies/project_policy_spec.rb20
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb68
-rw-r--r--spec/requests/api/commits_spec.rb53
-rw-r--r--spec/requests/api/features_spec.rb34
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb34
-rw-r--r--spec/requests/api/internal_spec.rb5
-rw-r--r--spec/requests/api/issues_spec.rb186
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/labels_spec.rb5
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb31
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/project_milestones_spec.rb74
-rw-r--r--spec/requests/api/project_templates_spec.rb28
-rw-r--r--spec/requests/api/projects_spec.rb151
-rw-r--r--spec/requests/api/release/links_spec.rb16
-rw-r--r--spec/requests/api/runner_spec.rb85
-rw-r--r--spec/requests/api/runners_spec.rb12
-rw-r--r--spec/requests/api/search_spec.rb4
-rw-r--r--spec/requests/api/users_spec.rb28
-rw-r--r--spec/requests/git_http_spec.rb134
-rw-r--r--spec/routing/import_routing_spec.rb15
-rw-r--r--spec/serializers/diff_file_entity_spec.rb8
-rw-r--r--spec/serializers/environment_serializer_spec.rb9
-rw-r--r--spec/serializers/namespace_basic_entity_spec.rb18
-rw-r--r--spec/serializers/namespace_serializer_spec.rb9
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb14
-rw-r--r--spec/serializers/project_import_entity_spec.rb22
-rw-r--r--spec/serializers/project_serializer_spec.rb44
-rw-r--r--spec/serializers/provider_repo_entity_spec.rb24
-rw-r--r--spec/serializers/provider_repo_serializer_spec.rb9
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb34
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb222
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb77
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb26
-rw-r--r--spec/services/error_tracking/list_projects_service_spec.rb4
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb12
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb201
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb8
-rw-r--r--spec/services/prometheus/adapter_service_spec.rb10
-rw-r--r--spec/services/users/activity_service_spec.rb12
-rw-r--r--spec/services/web_hook_service_spec.rb2
-rw-r--r--spec/support/api/milestones_shared_examples.rb5
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb103
-rw-r--r--spec/support/helpers/file_mover_helpers.rb12
-rw-r--r--spec/support/helpers/graphql_helpers.rb13
-rw-r--r--spec/support/helpers/login_helpers.rb13
-rw-r--r--spec/support/helpers/reactive_caching_helpers.rb2
-rw-r--r--spec/support/helpers/test_env.rb3
-rw-r--r--spec/support/matchers/graphql_matchers.rb4
-rw-r--r--spec/support/matchers/not_changed_matcher.rb3
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb2
-rw-r--r--spec/support/shared_examples/graphql/issuable_state_shared_examples.rb5
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb13
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb31
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb32
-rw-r--r--spec/support/shared_examples/requests/api/notes.rb42
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_examples.rb8
-rw-r--r--spec/support/shared_examples/services/boards/issues_move_service.rb16
-rw-r--r--spec/uploaders/file_mover_spec.rb33
-rw-r--r--spec/validators/sha_validator_spec.rb40
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb8
-rw-r--r--spec/views/projects/commits/_commit.html.haml_spec.rb59
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb20
-rw-r--r--spec/workers/build_finished_worker_spec.rb19
-rw-r--r--spec/workers/chat_notification_worker_spec.rb91
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb2
279 files changed, 7157 insertions, 1567 deletions
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb
new file mode 100644
index 00000000000..01ed81964c3
--- /dev/null
+++ b/spec/config/application_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Application do # rubocop:disable RSpec/FilePath
+ using RSpec::Parameterized::TableSyntax
+
+ FILTERED_PARAM = ActionDispatch::Http::ParameterFilter::FILTERED
+
+ context 'when parameters are logged' do
+ describe 'rails does not leak confidential parameters' do
+ def request_for_url(input_url)
+ env = Rack::MockRequest.env_for(input_url)
+ env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters
+
+ ActionDispatch::Request.new(env)
+ end
+
+ where(:input_url, :output_query) do
+ '/' | {}
+ '/?safe=1' | { 'safe' => '1' }
+ '/?private_token=secret' | { 'private_token' => FILTERED_PARAM }
+ '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => FILTERED_PARAM }
+ '/?note=secret&noteable=1&prefix_note=2' | { 'note' => FILTERED_PARAM, 'noteable' => '1', 'prefix_note' => '2' }
+ '/?note[note]=secret&target_type=1' | { 'note' => FILTERED_PARAM, 'target_type' => '1' }
+ '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => FILTERED_PARAM }, 'target_type' => '1' }
+ end
+
+ with_them do
+ it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/appearances_controller_spec.rb b/spec/controllers/admin/appearances_controller_spec.rb
new file mode 100644
index 00000000000..4ddd0953267
--- /dev/null
+++ b/spec/controllers/admin/appearances_controller_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Admin::AppearancesController do
+ let(:admin) { create(:admin) }
+ let(:header_message) { "Header message" }
+ let(:footer_message) { "Footer" }
+
+ describe 'POST #create' do
+ let(:create_params) do
+ {
+ title: "Foo",
+ description: "Bar",
+ header_message: header_message,
+ footer_message: footer_message
+ }
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'creates appearance with footer and header message' do
+ post :create, params: { appearance: create_params }
+
+ expect(Appearance.current).to have_attributes(
+ header_message: header_message,
+ footer_message: footer_message
+ )
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:update_params) do
+ {
+ header_message: header_message,
+ footer_message: footer_message
+ }
+ end
+
+ before do
+ create(:appearance)
+
+ sign_in(admin)
+ end
+
+ it 'updates appearance with footer and header message' do
+ put :update, params: { appearance: update_params }
+
+ expect(Appearance.current).to have_attributes(
+ header_message: header_message,
+ footer_message: footer_message
+ )
+ end
+ end
+end
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 4cf14030ca1..82e24213408 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -1,18 +1,35 @@
require 'spec_helper'
describe Admin::RunnersController do
- let(:runner) { create(:ci_runner) }
+ let!(:runner) { create(:ci_runner) }
before do
sign_in(create(:admin))
end
describe '#index' do
+ render_views
+
it 'lists all runners' do
get :index
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'avoids N+1 queries', :request_store do
+ get :index
+
+ control_count = ActiveRecord::QueryRecorder.new { get :index }.count
+
+ create(:ci_runner, :tagged_only)
+
+ # There is still an N+1 query for `runner.builds.count`
+ expect { get :index }.not_to exceed_query_limit(control_count + 1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to have_content('tag1')
+ expect(response.body).to have_content('tag2')
+ end
end
describe '#show' do
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 6b66cbd2651..cb24a6ef142 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -8,6 +8,31 @@ describe Admin::UsersController do
sign_in(admin)
end
+ describe 'GET #index' do
+ it 'retrieves all users' do
+ get :index
+
+ expect(assigns(:users)).to match_array([user, admin])
+ end
+
+ it 'filters by admins' do
+ get :index, params: { filter: 'admins' }
+
+ expect(assigns(:users)).to eq([admin])
+ end
+ end
+
+ describe 'GET :id' do
+ it 'finds a user case-insensitively' do
+ user = create(:user, username: 'CaseSensitive')
+
+ get :show, params: { id: user.username.downcase }
+
+ expect(response).to be_redirect
+ expect(response.location).to end_with(user.username)
+ end
+ end
+
describe 'DELETE #user with projects' do
let(:project) { create(:project, namespace: user.namespace) }
let!(:issue) { create(:issue, author: user) }
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index c9e520317e8..dca74bd5f84 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -665,6 +665,14 @@ describe ApplicationController do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
end
+
+ it 'does not set the "no-store" header for XHR requests' do
+ sign_in(user)
+
+ get :index, xhr: true
+
+ expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate'
+ end
end
end
end
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index 307c5d60c57..8580900215c 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -112,7 +112,8 @@ describe IssuableCollections do
assignee_username: 'user1',
author_id: '2',
author_username: 'user2',
- authorized_only: 'true',
+ authorized_only: 'yes',
+ confidential: true,
due_date: '2017-01-01',
group_id: '3',
iids: '4',
@@ -140,6 +141,7 @@ describe IssuableCollections do
'assignee_username' => 'user1',
'author_id' => '2',
'author_username' => 'user2',
+ 'confidential' => true,
'label_name' => 'foo',
'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup',
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index ddf33ebad16..ab40b4eb178 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -3,11 +3,9 @@ require 'spec_helper'
describe Dashboard::MilestonesController do
let(:project) { create(:project) }
let(:group) { create(:group) }
- let(:public_group) { create(:group, :public) }
let(:user) { create(:user) }
let(:project_milestone) { create(:milestone, project: project) }
let(:group_milestone) { create(:milestone, group: group) }
- let!(:public_milestone) { create(:milestone, group: public_group) }
let(:milestone) do
DashboardMilestone.build(
[project],
@@ -45,6 +43,9 @@ describe Dashboard::MilestonesController do
end
describe "#index" do
+ let(:public_group) { create(:group, :public) }
+ let!(:public_milestone) { create(:milestone, group: public_group) }
+
render_views
it 'returns group and project milestones to which the user belongs' do
@@ -74,10 +75,10 @@ describe Dashboard::MilestonesController do
expect(response.body).not_to include(project_milestone.title)
end
- it 'should contain group and project milestones to which the user belongs to' do
+ it 'should show counts of group and project milestones to which the user belongs to' do
get :index
- expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>")
+ expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>")
expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
end
end
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
index 1e8e82da4f3..d9ba85cf56a 100644
--- a/spec/controllers/google_api/authorizations_controller_spec.rb
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -6,7 +6,7 @@ describe GoogleApi::AuthorizationsController do
let(:token) { 'token' }
let(:expires_at) { 1.hour.since.strftime('%s') }
- subject { get :callback, params: { code: 'xxx', state: @state } }
+ subject { get :callback, params: { code: 'xxx', state: state } }
before do
sign_in(user)
@@ -15,35 +15,57 @@ describe GoogleApi::AuthorizationsController do
.to receive(:get_token).and_return([token, expires_at])
end
- it 'sets token and expires_at in session' do
- subject
+ shared_examples_for 'access denied' do
+ it 'returns a 404' do
+ subject
- expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
- .to eq(token)
- expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
- .to eq(expires_at)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
+ expect(response).to have_http_status(:not_found)
+ end
end
- context 'when redirect uri key is stored in state' do
- set(:project) { create(:project) }
- let(:redirect_uri) { project_clusters_url(project).to_s }
+ context 'session key is present' do
+ let(:session_key) { 'session-key' }
+ let(:redirect_uri) { 'example.com' }
before do
- @state = GoogleApi::CloudPlatform::Client
- .new_session_key_for_redirect_uri do |key|
- session[key] = redirect_uri
+ session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri
+ end
+
+ context 'session key matches state param' do
+ let(:state) { session_key }
+
+ it 'sets token and expires_at in session' do
+ subject
+
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
+ .to eq(token)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
+ .to eq(expires_at)
+ end
+
+ it 'redirects to the URL stored in state param' do
+ expect(subject).to redirect_to(redirect_uri)
end
end
- it 'redirects to the URL stored in state param' do
- expect(subject).to redirect_to(redirect_uri)
+ context 'session key does not match state param' do
+ let(:state) { 'bad-key' }
+
+ it_behaves_like 'access denied'
end
- end
- context 'when redirection url is not stored in state' do
- it 'redirects to root_path' do
- expect(subject).to redirect_to(root_path)
+ context 'state param is blank' do
+ let(:state) { '' }
+
+ it_behaves_like 'access denied'
end
end
+
+ context 'state param is present, but session key is blank' do
+ let(:state) { 'session-key' }
+
+ it_behaves_like 'access denied'
+ end
end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index 360030102e0..ef23ffaa843 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -453,7 +453,7 @@ describe Groups::ClustersController do
end
context 'when domain is invalid' do
- let(:domain) { 'not-a-valid-domain' }
+ let(:domain) { 'http://not-a-valid-domain' }
it 'should not update cluster attributes' do
go
diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb
index dab7700cf64..b0c20fb5a90 100644
--- a/spec/controllers/groups/shared_projects_controller_spec.rb
+++ b/spec/controllers/groups/shared_projects_controller_spec.rb
@@ -6,6 +6,8 @@ describe Groups::SharedProjectsController do
end
def share_project(project)
+ group.add_developer(user)
+
Projects::GroupLinks::CreateService.new(
project,
user,
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
index 5ba64ab3eed..8cbec79095f 100644
--- a/spec/controllers/import/gitea_controller_spec.rb
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -40,4 +40,12 @@ describe Import::GiteaController do
end
end
end
+
+ describe "GET realtime_changes" do
+ it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' do
+ before do
+ assign_host_url
+ end
+ end
+ end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index bca5f3f6589..162dff98ec5 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -60,4 +60,8 @@ describe Import::GithubController do
describe "POST create" do
it_behaves_like 'a GitHub-ish import controller: POST create'
end
+
+ describe "GET realtime_changes" do
+ it_behaves_like 'a GitHub-ish import controller: GET realtime_changes'
+ end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 232a5e2793b..e0da23ca0b8 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -193,7 +193,7 @@ describe OmniauthCallbacksController, type: :controller do
before do
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config] })
- mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response)
+ mock_auth_hash_with_saml_xml('saml', 'my-uid', user.email, mock_saml_response)
request.env["devise.mapping"] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
post :saml, params: { SAMLResponse: mock_saml_response }
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index 760c0fab130..ee881f85233 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -43,7 +43,8 @@ describe Profiles::PreferencesController do
color_scheme_id: '1',
dashboard: 'stars',
theme_id: '2',
- first_day_of_week: '1'
+ first_day_of_week: '1',
+ preferred_language: 'jp'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(ActionController::Parameters.new(prefs).permit!)
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
new file mode 100644
index 00000000000..a9a058e7e17
--- /dev/null
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::AutocompleteSourcesController do
+ set(:group) { create(:group) }
+ set(:project) { create(:project, namespace: group) }
+ set(:issue) { create(:issue, project: project) }
+ set(:user) { create(:user) }
+
+ describe 'GET members' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'returns an array of member object' do
+ get :members, format: :json, params: { namespace_id: group.path, project_id: project.path, type: issue.class.name, type_id: issue.id }
+
+ all = json_response.find {|member| member["username"] == 'all'}
+ the_group = json_response.find {|member| member["username"] == group.full_path}
+ the_user = json_response.find {|member| member["username"] == user.username}
+
+ expect(all.symbolize_keys).to include(username: 'all',
+ name: 'All Project and Group Members',
+ count: 1)
+
+ expect(the_group.symbolize_keys).to include(type: group.class.name,
+ name: group.full_name,
+ avatar_url: group.avatar_url,
+ count: 1)
+
+ expect(the_user.symbolize_keys).to include(type: user.class.name,
+ name: user.name,
+ avatar_url: user.avatar_url)
+ end
+ end
+
+ describe 'GET milestones' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let!(:project_milestone) { create(:milestone, project: project) }
+ let!(:group_milestone) { create(:milestone, group: group) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'lists milestones' do
+ group.add_owner(user)
+
+ get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ milestone_titles = json_response.map { |milestone| milestone["title"] }
+ expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title])
+ end
+
+ context 'when user cannot read project issues and merge requests' do
+ it 'renders 404' do
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index aa97a417a98..36ce1119100 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -54,9 +54,9 @@ describe Projects::EnvironmentsController do
it 'responds with a flat payload describing available environments' do
expect(environments.count).to eq 3
- expect(environments.first['name']).to eq 'production'
- expect(environments.second['name']).to eq 'staging/review-1'
- expect(environments.third['name']).to eq 'staging/review-2'
+ expect(environments.first).to include('name' => 'production', 'name_without_type' => 'production')
+ expect(environments.second).to include('name' => 'staging/review-1', 'name_without_type' => 'review-1')
+ expect(environments.third).to include('name' => 'staging/review-2', 'name_without_type' => 'review-2')
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
@@ -155,9 +155,9 @@ describe Projects::EnvironmentsController do
expect(response).to be_ok
expect(response).not_to render_template 'folder'
expect(json_response['environments'][0])
- .to include('name' => 'staging-1.0/review')
+ .to include('name' => 'staging-1.0/review', 'name_without_type' => 'review')
expect(json_response['environments'][1])
- .to include('name' => 'staging-1.0/zzz')
+ .to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz')
end
end
end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 73fb7307e11..8decd8f1382 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -24,4 +24,20 @@ describe Projects::GraphsController do
expect(response).to redirect_to action: :charts
end
end
+
+ describe 'charts' do
+ context 'when languages were previously detected' do
+ let!(:repository_language) { create(:repository_language, project: project) }
+
+ it 'sets the languages properly' do
+ get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
+
+ expect(assigns[:languages]).to eq(
+ [value: repository_language.share,
+ label: repository_language.name,
+ color: repository_language.color,
+ highlight: repository_language.color])
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 675eeff8d12..ce021b2f085 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -65,8 +65,24 @@ describe Projects::GroupLinksController do
end
end
+ context 'when user does not have access to the public group' do
+ let(:group) { create(:group, :public) }
+
+ include_context 'link project to group'
+
+ it 'renders 404' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not share project with that group' do
+ expect(group.shared_projects).not_to include project
+ end
+ end
+
context 'when project group id equal link group id' do
before do
+ group2.add_developer(user)
+
post(:create, params: {
namespace_id: project.namespace,
project_id: project,
@@ -102,5 +118,26 @@ describe Projects::GroupLinksController do
expect(flash[:alert]).to eq('Please select a group.')
end
end
+
+ context 'when link is not persisted in the database' do
+ before do
+ allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
+ .and_return({ status: :error, http_status: 409, message: 'error' })
+
+ post(:create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ link_group_id: group.id,
+ link_group_access: ProjectGroupLink.default_access
+ })
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ project_project_members_path(project)
+ )
+ expect(flash[:alert]).to eq('error')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index a6017d8e5e6..e85f32d6e30 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -4,10 +4,11 @@ describe Projects::MergeRequests::DiffsController do
include ProjectForksHelper
let(:project) { create(:project, :repository) }
- let(:user) { project.owner }
+ let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
before do
+ project.add_maintainer(user)
sign_in(user)
end
@@ -114,16 +115,6 @@ describe Projects::MergeRequests::DiffsController do
expect(paths).to include(existing_path)
end
end
-
- context 'when the path does not exist in the diff' do
- before do
- diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb')
- end
-
- it 'returns a 404' do
- expect(response).to have_gitlab_http_status(404)
- end
- end
end
context 'when the user cannot view the merge request' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 81892575889..0b0f5117784 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -252,8 +252,8 @@ describe Projects::NotesController do
note: 'some note',
noteable_id: merge_request.id.to_s,
noteable_type: 'MergeRequest',
- merge_request_diff_head_sha: 'sha',
- in_reply_to_discussion_id: nil
+ commit_id: nil,
+ merge_request_diff_head_sha: 'sha'
}).permit!
expect(Notes::CreateService).to receive(:new).with(project, user, service_params).and_return(double(execute: true))
@@ -266,6 +266,22 @@ describe Projects::NotesController do
end
end
+ context 'when creating a comment on a commit with SHA1 starting with a large number' do
+ let(:commit) { create(:commit, project: project, id: '842616594688d2351480dfebd67b3d8d15571e6d') }
+
+ it 'creates a note successfully' do
+ expect do
+ post :create, params: {
+ note: { note: 'some note', commit_id: commit.id },
+ namespace_id: project.namespace,
+ project_id: project,
+ target_type: 'commit',
+ target_id: commit.id
+ }
+ end.to change { Note.count }.by(1)
+ end
+ end
+
context 'when creating a commit comment from an MR fork' do
let(:project) { create(:project, :repository) }
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index 4b742a5d427..d6eece47804 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -42,6 +42,18 @@ describe Projects::PagesController do
expect(response).to have_gitlab_http_status(302)
end
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'returns 404 status' do
+ delete :destroy, params: request_params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
context 'pages disabled' do
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 5c6858dc7b2..77a94f26d8c 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -205,6 +205,8 @@ describe SnippetsController do
end
context 'when the snippet description contains a file' do
+ include FileMoverHelpers
+
let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
let(:text_file) { '/-/system/temp/secret78/text.txt' }
let(:description) do
@@ -215,6 +217,8 @@ describe SnippetsController do
before do
allow(FileUtils).to receive(:mkdir_p)
allow(FileUtils).to receive(:move)
+ stub_file_mover(text_file)
+ stub_file_mover(picture_file)
end
subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) }
diff --git a/spec/factories/import_state.rb b/spec/factories/import_states.rb
index d6de26dccbc..d6de26dccbc 100644
--- a/spec/factories/import_state.rb
+++ b/spec/factories/import_states.rb
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 1b12f84d7b8..e7fd22a96b2 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -1,13 +1,14 @@
FactoryBot.define do
factory :personal_access_token do
user
- token { SecureRandom.hex(50) }
sequence(:name) { |n| "PAT #{n}" }
revoked false
expires_at { 5.days.from_now }
scopes ['api']
impersonation false
+ after(:build) { |personal_access_token| personal_access_token.ensure_token }
+
trait :impersonation do
impersonation true
end
@@ -21,7 +22,7 @@ FactoryBot.define do
end
trait :invalid do
- token nil
+ token_digest nil
end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f7ef34d773b..30d3b22d868 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -313,6 +313,20 @@ FactoryBot.define do
end
end
+ factory :youtrack_project, parent: :project do
+ has_external_issue_tracker true
+
+ after :create do |project|
+ project.create_youtrack_service(
+ active: true,
+ properties: {
+ 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack',
+ 'issues_url' => 'http://youtrack/issues/:id'
+ }
+ )
+ end
+ end
+
factory :jira_project, parent: :project do
has_external_issue_tracker true
jira_service
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index a47bd7cafca..1d2b724a5e5 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -73,11 +73,16 @@ FactoryBot.define do
end
after(:create) do |user, evaluator|
- user.identities << create(
- :identity,
+ identity_attrs = {
provider: evaluator.provider,
extern_uid: evaluator.extern_uid
- )
+ }
+
+ if evaluator.respond_to?(:saml_provider)
+ identity_attrs[:saml_provider] = evaluator.saml_provider
+ end
+
+ user.identities << create(:identity, identity_attrs)
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 57215c0d1e9..83cd686818c 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -39,6 +39,38 @@ describe 'Admin Appearance' do
expect_custom_new_project_appearance(appearance)
end
+ context 'Custom system header and footer' do
+ before do
+ sign_in(create(:admin))
+ end
+
+ context 'when system header and footer messages are empty' do
+ it 'shows custom system header and footer fields' do
+ visit admin_appearances_path
+
+ expect(page).to have_field('appearance_header_message', with: '')
+ expect(page).to have_field('appearance_footer_message', with: '')
+ expect(page).to have_field('appearance_message_background_color')
+ expect(page).to have_field('appearance_message_font_color')
+ end
+ end
+
+ context 'when system header and footer messages are not empty' do
+ before do
+ appearance.update(header_message: 'Foo', footer_message: 'Bar')
+ end
+
+ it 'shows custom system header and footer fields' do
+ visit admin_appearances_path
+
+ expect(page).to have_field('appearance_header_message', with: appearance.header_message)
+ expect(page).to have_field('appearance_footer_message', with: appearance.footer_message)
+ expect(page).to have_field('appearance_message_background_color')
+ expect(page).to have_field('appearance_message_font_color')
+ end
+ end
+ end
+
it 'Custom sign-in page' do
visit new_user_session_path
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 6c4b04ab76b..9d1c1e3acc7 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -114,7 +114,16 @@ describe 'Dashboard Projects' do
end
end
- context 'when on Starred projects tab' do
+ context 'when on Starred projects tab', :js do
+ it 'shows the empty state when there are no starred projects' do
+ visit(starred_dashboard_projects_path)
+
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content("You don't have starred projects yet.")
+ expect(element.find('.svg-content img')['src']).to have_content('illustrations/starred_empty')
+ end
+
it 'shows only starred projects' do
user.toggle_star(project2)
diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb
index cbddf117462..55f5ff04d01 100644
--- a/spec/features/dashboard/shortcuts_spec.rb
+++ b/spec/features/dashboard/shortcuts_spec.rb
@@ -44,7 +44,7 @@ describe 'Dashboard shortcuts', :js do
find('body').send_keys([:shift, 'S'])
find('.nothing-here-block')
- expect(page).to have_selector('.snippets-list-holder')
+ expect(page).to have_content('No snippets found')
find('body').send_keys([:shift, 'P'])
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
index fb4263d74c4..0e248c8732d 100644
--- a/spec/features/dashboard/snippets_spec.rb
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -13,6 +13,21 @@ describe 'Dashboard snippets' do
it_behaves_like 'paginated snippets'
end
+ context 'when there are no project snippets', :js do
+ let(:project) { create(:project, :public) }
+ before do
+ sign_in(project.owner)
+ visit dashboard_snippets_path
+ end
+
+ it 'shows the empty state when there are no snippets' do
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content("Snippets are small pieces of code or notes that you want to keep.")
+ expect(element.find('.svg-content img')['src']).to have_content('illustrations/snippets_empty')
+ end
+ end
+
context 'filtering by visibility' do
let(:user) { create(:user) }
let!(:snippets) do
diff --git a/spec/features/display_system_header_and_footer_bar_spec.rb b/spec/features/display_system_header_and_footer_bar_spec.rb
new file mode 100644
index 00000000000..af9d9a5834f
--- /dev/null
+++ b/spec/features/display_system_header_and_footer_bar_spec.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'Display system header and footer bar' do
+ let(:header_message) { "Foo" }
+ let(:footer_message) { "Bar" }
+
+ shared_examples 'system header is configured' do
+ it 'shows system header' do
+ expect(page).to have_css('.header-message')
+ end
+
+ it 'shows the correct content' do
+ page.within('.header-message') do
+ expect(page).to have_content(header_message)
+ end
+ end
+ end
+
+ shared_examples 'system footer is configured' do
+ it 'shows system footer' do
+ expect(page).to have_css('.footer-message')
+ end
+
+ it 'shows the correct content' do
+ page.within('.footer-message') do
+ expect(page).to have_content(footer_message)
+ end
+ end
+ end
+
+ shared_examples 'system header is not configured' do
+ it 'does not show system header' do
+ expect(page).not_to have_css('.header-message')
+ end
+ end
+
+ shared_examples 'system footer is not configured' do
+ it 'does not show system footer' do
+ expect(page).not_to have_css('.footer-message')
+ end
+ end
+
+ context 'when authenticated' do
+ context 'when system header and footer are not configured' do
+ before do
+ sign_in(create(:user))
+
+ visit root_path
+ end
+
+ it_behaves_like 'system header is not configured'
+ it_behaves_like 'system footer is not configured'
+ end
+
+ context 'when only system header is defined' do
+ before do
+ create(:appearance, header_message: header_message)
+
+ sign_in(create(:user))
+ visit root_path
+ end
+
+ it_behaves_like 'system header is configured'
+ it_behaves_like 'system footer is not configured'
+ end
+
+ context 'when only system footer is defined' do
+ before do
+ create(:appearance, footer_message: footer_message)
+
+ sign_in(create(:user))
+ visit root_path
+ end
+
+ it_behaves_like 'system header is not configured'
+ it_behaves_like 'system footer is configured'
+ end
+
+ context 'when system header and footer are defined' do
+ before do
+ create(:appearance, header_message: header_message, footer_message: footer_message)
+
+ sign_in(create(:user))
+ visit root_path
+ end
+
+ it_behaves_like 'system header is configured'
+ it_behaves_like 'system footer is configured'
+ end
+ end
+
+ context 'when not authenticated' do
+ context 'when system header and footer are not configured' do
+ before do
+ visit root_path
+ end
+
+ it_behaves_like 'system header is not configured'
+ it_behaves_like 'system footer is not configured'
+ end
+
+ context 'when only system header is defined' do
+ before do
+ create(:appearance, header_message: header_message)
+
+ visit root_path
+ end
+
+ it_behaves_like 'system header is configured'
+ it_behaves_like 'system footer is not configured'
+ end
+
+ context 'when only system footer is defined' do
+ before do
+ create(:appearance, footer_message: footer_message)
+
+ visit root_path
+ end
+
+ it_behaves_like 'system header is not configured'
+ it_behaves_like 'system footer is configured'
+ end
+
+ context 'when system header and footer are defined' do
+ before do
+ create(:appearance, header_message: header_message, footer_message: footer_message)
+
+ visit root_path
+ end
+
+ it_behaves_like 'system header is configured'
+ it_behaves_like 'system footer is configured'
+ end
+ end
+end
diff --git a/spec/features/groups/labels/create_spec.rb b/spec/features/groups/labels/create_spec.rb
new file mode 100644
index 00000000000..f5062a65321
--- /dev/null
+++ b/spec/features/groups/labels/create_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Create a group label' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ visit group_labels_path(group)
+ end
+
+ it 'creates a new label' do
+ click_link 'New label'
+ fill_in 'Title', with: 'test-label'
+ click_button 'Create label'
+
+ expect(page).to have_content 'test-label'
+ expect(current_path).to eq(group_labels_path(group))
+ end
+end
diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb
index 0ce7dad4040..62308d3b518 100644
--- a/spec/features/groups/labels/index_spec.rb
+++ b/spec/features/groups/labels/index_spec.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Group labels' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let!(:label) { create(:group_label, group: group) }
+ let!(:label2) { create(:group_label) }
before do
group.add_owner(user)
@@ -11,7 +14,16 @@ describe 'Group labels' do
visit group_labels_path(group)
end
- it 'label has edit button', :js do
+ it 'shows labels that belong to the group' do
+ expect(page).to have_content(label.name)
+ expect(page).not_to have_content(label2.name)
+ end
+
+ it 'shows a new label button' do
+ expect(page).to have_link('New label')
+ end
+
+ it 'shows an edit label button', :js do
expect(page).to have_selector('.label-action.edit')
end
end
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 2f45ef856a5..7b6e9cd66b2 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -28,6 +28,22 @@ describe 'issuable list' do
expect(first('.fa-thumbs-down').find(:xpath, '..')).to have_content(1)
expect(first('.fa-comments').find(:xpath, '..')).to have_content(2)
end
+
+ it 'sorts labels alphabetically' do
+ label1 = create(:label, project: project, title: 'a')
+ label2 = create(:label, project: project, title: 'z')
+ label3 = create(:label, project: project, title: 'X')
+ label4 = create(:label, project: project, title: 'B')
+ issuable = create_issuable(issuable_type)
+ issuable.labels << [label1, label2, label3, label4]
+
+ visit_issuable_list(issuable_type)
+
+ expect(all('.label-link')[0].text).to have_content('B')
+ expect(all('.label-link')[1].text).to have_content('X')
+ expect(all('.label-link')[2].text).to have_content('a')
+ expect(all('.label-link')[3].text).to have_content('z')
+ end
end
it "counts merge requests closing issues icons for each issue" do
@@ -45,6 +61,14 @@ describe 'issuable list' do
end
end
+ def create_issuable(issuable_type)
+ if issuable_type == :issue
+ create(:issue, project: project)
+ else
+ create(:merge_request, source_project: project)
+ end
+ end
+
def create_issuables(issuable_type)
3.times do |n|
issuable =
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 0e296ab2109..096756f19cc 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -66,7 +66,7 @@ describe 'Dropdown hint', :js do
it 'filters with text' do
filtered_search.set('a')
- expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 4)
+ expect(find(js_dropdown_hint)).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
end
end
@@ -119,6 +119,15 @@ describe 'Dropdown hint', :js do
expect_tokens([{ name: 'my-reaction' }])
expect_filtered_search_input_empty
end
+
+ it 'opens the yes-no dropdown when you click on confidential' do
+ click_hint('confidential')
+
+ expect(page).to have_css(js_dropdown_hint, visible: false)
+ expect(page).to have_css('#js-dropdown-confidential', visible: true)
+ expect_tokens([{ name: 'confidential' }])
+ expect_filtered_search_input_empty
+ end
end
describe 'selecting from dropdown with some input' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 8abab3f35d6..da23aea1fc9 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -86,7 +86,7 @@ describe 'Search bar', :js do
expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: original_size)
end
- it 'resets the dropdown filters' do
+ it 'resets the dropdown filters', :quarantine do
filtered_search.click
hint_offset = get_left_style(find('#js-dropdown-hint')['style'])
@@ -100,7 +100,7 @@ describe 'Search bar', :js do
find('.filtered-search-box .clear-search').click
filtered_search.click
- expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 5)
+ expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
expect(get_left_style(find('#js-dropdown-hint')['style'])).to eq(hint_offset)
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index a4c34ce85f0..9fd661d80ae 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -59,13 +59,6 @@ describe 'Visual tokens', :js do
expect(page).to have_css('#js-dropdown-author', visible: false)
end
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-author', visible: false)
- end
-
describe 'selecting different author from dropdown' do
before do
filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
@@ -109,13 +102,6 @@ describe 'Visual tokens', :js do
expect(page).to have_css('#js-dropdown-assignee', visible: false)
end
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-assignee', visible: false)
- end
-
describe 'selecting static option from dropdown' do
before do
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
@@ -167,13 +153,6 @@ describe 'Visual tokens', :js do
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-milestone', visible: false)
end
-
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-milestone', visible: false)
- end
end
describe 'editing label token' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index c22ad0d20ef..986f3823275 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -278,7 +278,7 @@ describe 'GFM autocomplete', :js do
end
end
- # This context has jsut one example in each contexts in order to improve spec performance.
+ # This context has just one example in each contexts in order to improve spec performance.
context 'labels', :quarantine do
let!(:backend) { create(:label, project: project, title: 'backend') }
let!(:bug) { create(:label, project: project, title: 'bug') }
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index d19408ee87f..c837a6752f9 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -222,6 +222,11 @@ describe 'Merge request > User creates image diff notes', :js do
end
def create_image_diff_note
+ expand_text = 'Click to expand it.'
+ page.all('a', text: expand_text).each do |element|
+ element.click
+ end
+
find('.js-add-image-diff-note-button', match: :first).click
find('.diff-content .note-textarea').native.send_keys('image diff test comment')
click_button 'Comment'
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 38b4e4a6d1b..ea2bb1503bb 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -8,6 +8,8 @@ describe "User creates a merge request", :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(approval_rules: false)
+
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index aa91ade46ca..5c45e363997 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -1,7 +1,11 @@
require 'rails_helper'
describe 'Merge request > User sees versions', :js do
- let(:merge_request) { create(:merge_request, importing: true) }
+ let(:merge_request) do
+ create(:merge_request).tap do |mr|
+ mr.merge_request_diff.destroy
+ end
+ end
let(:project) { merge_request.source_project }
let(:user) { project.creator }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index d3050760c06..2aa0177af5d 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -7,6 +7,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
end
end
+ let(:admin) { create(:admin) }
+
around do |example|
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
example.run
@@ -16,6 +18,7 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
it 'User sees their active sessions' do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
+ Capybara::Session.new(:session3)
# note: headers can only be set on the non-js (aka. rack-test) driver
using_session :session1 do
@@ -37,9 +40,27 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
gitlab_sign_in(user)
end
+ # set an admin session impersonating the user
+ using_session :session3 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
+ )
+
+ gitlab_sign_in(admin)
+
+ visit admin_user_path(user)
+
+ click_link 'Impersonate'
+ end
+
using_session :session1 do
visit profile_active_sessions_path
+ expect(page).to(
+ have_selector('ul.list-group li.list-group-item', { text: 'Signed in on',
+ count: 2 }))
+
expect(page).to have_content(
'127.0.0.1 ' \
'This is your current session ' \
@@ -57,33 +78,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
)
expect(page).to have_selector '[title="Smartphone"]', count: 1
- end
- end
-
- it 'User can revoke a session', :js, :redis_session_store do
- Capybara::Session.new(:session1)
- Capybara::Session.new(:session2)
-
- # set an additional session in another browser
- using_session :session2 do
- gitlab_sign_in(user)
- end
-
- using_session :session1 do
- gitlab_sign_in(user)
- visit profile_active_sessions_path
-
- expect(page).to have_link('Revoke', count: 1)
-
- accept_confirm { click_on 'Revoke' }
-
- expect(page).not_to have_link('Revoke')
- end
-
- using_session :session2 do
- visit profile_active_sessions_path
- expect(page).to have_content('You need to sign in or sign up before continuing.')
+ expect(page).not_to have_content('Chrome on Windows')
end
end
end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 90d6841af0e..9909bfb5904 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User visits the profile preferences page' do
+ include Select2Helper
+
let(:user) { create(:user) }
before do
@@ -60,6 +62,28 @@ describe 'User visits the profile preferences page' do
end
end
+ describe 'User changes their language', :js do
+ it 'creates a flash message' do
+ select2('en', from: '#user_preferred_language')
+ click_button 'Save'
+
+ wait_for_requests
+
+ expect_preferences_saved_message
+ end
+
+ it 'updates their preference' do
+ wait_for_requests
+ select2('eo', from: '#user_preferred_language')
+ click_button 'Save'
+
+ wait_for_requests
+ refresh
+
+ expect(page).to have_css('html[lang="eo"]')
+ end
+ end
+
def expect_preferences_saved_message
page.within('.flash-container') do
expect(page).to have_content('Preferences saved.')
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 3edcc7ac2cd..a7aa63018fd 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -548,10 +548,7 @@ describe 'File blob', :js do
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows names of dependency manager and package
- expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.')
-
- # shows a link to the gem
- expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord')
+ expect(page).to have_content('This project manages its dependencies using RubyGems.')
# shows a learn more link
expect(page).to have_link('Learn more', href: 'https://rubygems.org/')
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 6e6c299ee2e..1522a3361a1 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -77,7 +77,7 @@ describe 'Editing file blob', :js do
click_link 'Preview'
wait_for_requests
- # the above generates two seperate lists (not embedded) in CommonMark
+ # the above generates two separate lists (not embedded) in CommonMark
expect(page).to have_content("sublist")
expect(page).not_to have_xpath("//ol//li//ul")
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 97757e8da92..ee71c843b80 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -229,6 +229,38 @@ describe 'Branches' do
end
end
+ describe 'comparing branches' do
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ shared_examples 'compares branches' do
+ it 'compares branches' do
+ visit project_branches_path(project)
+
+ page.within first('.all-branches li') do
+ click_link 'Compare'
+ end
+
+ expect(page).to have_content 'Commits'
+ expect(page).to have_link 'Create merge request'
+ end
+ end
+
+ context 'on a read-only instance' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it_behaves_like 'compares branches'
+ end
+
+ context 'on a read-write instance' do
+ it_behaves_like 'compares branches'
+ end
+ end
+
def sorted_branches(repository, count:, sort_by:, state: nil)
branches = repository.branches_sorted_by(sort_by)
branches = branches.select { |b| state == 'active' ? b.active? : b.stale? } if state
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index fceead0b45e..b2d2dba55f1 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do
before do
project.add_maintainer(maintainer)
+ group_to_share_with.add_guest(maintainer)
sign_in(maintainer)
end
@@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do
before do
project.add_maintainer(maintainer)
+ group.add_guest(maintainer)
sign_in(maintainer)
visit project_settings_members_path(project)
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 435fb229b69..f564ae34f11 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -13,16 +13,6 @@ describe 'Pages' do
sign_in(user)
end
- shared_examples 'no pages deployed' do
- it 'does not see anything to destroy' do
- visit project_pages_path(project)
-
- expect(page).to have_content('Configure pages')
- expect(page).not_to have_link('Remove pages')
- expect(page).not_to have_text('Only the project owner can remove pages')
- end
- end
-
context 'when user is the owner' do
before do
project.namespace.update(owner: user)
@@ -181,7 +171,12 @@ describe 'Pages' do
end
end
- it_behaves_like 'no pages deployed'
+ it 'does not see anything to destroy' do
+ visit project_pages_path(project)
+
+ expect(page).to have_content('Configure pages')
+ expect(page).not_to have_link('Remove pages')
+ end
describe 'project settings page' do
it 'renders "Pages" tab' do
@@ -208,22 +203,6 @@ describe 'Pages' do
end
end
- context 'when the user is not the owner' do
- context 'when pages deployed' do
- before do
- allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
- end
-
- it 'sees "Only the project owner can remove pages" text' do
- visit project_pages_path(project)
-
- expect(page).to have_text('Only the project owner can remove pages')
- end
- end
-
- it_behaves_like 'no pages deployed'
- end
-
describe 'HTTPS settings', :js, :https_pages_enabled do
before do
project.namespace.update(owner: user)
@@ -233,7 +212,7 @@ describe 'Pages' do
it 'tries to change the setting' do
visit project_pages_path(project)
- expect(page).to have_content("Force domains with SSL certificates to use HTTPS")
+ expect(page).to have_content("Force HTTPS (requires valid certificates)")
uncheck :project_pages_https_only
@@ -282,58 +261,52 @@ describe 'Pages' do
visit project_pages_path(project)
expect(page).not_to have_field(:project_pages_https_only)
- expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS')
+ expect(page).not_to have_content('Force HTTPS (requires valid certificates)')
expect(page).not_to have_button('Save')
end
end
end
describe 'Remove page' do
- context 'when user is the owner' do
- let(:project) { create :project, :repository }
-
- before do
- project.namespace.update(owner: user)
+ let(:project) { create :project, :repository }
+
+ context 'when pages are deployed' do
+ let(:pipeline) do
+ commit_sha = project.commit('HEAD').sha
+
+ project.ci_pipelines.create(
+ ref: 'HEAD',
+ sha: commit_sha,
+ source: :push,
+ protected: false
+ )
end
- context 'when pages are deployed' do
- let(:pipeline) do
- commit_sha = project.commit('HEAD').sha
-
- project.ci_pipelines.create(
- ref: 'HEAD',
- sha: commit_sha,
- source: :push,
- protected: false
- )
- end
-
- let(:ci_build) do
- create(
- :ci_build,
- project: project,
- pipeline: pipeline,
- ref: 'HEAD',
- legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')),
- legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta'))
- )
- end
+ let(:ci_build) do
+ create(
+ :ci_build,
+ project: project,
+ pipeline: pipeline,
+ ref: 'HEAD',
+ legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')),
+ legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta'))
+ )
+ end
- before do
- result = Projects::UpdatePagesService.new(project, ci_build).execute
- expect(result[:status]).to eq(:success)
- expect(project).to be_pages_deployed
- end
+ before do
+ result = Projects::UpdatePagesService.new(project, ci_build).execute
+ expect(result[:status]).to eq(:success)
+ expect(project).to be_pages_deployed
+ end
- it 'removes the pages' do
- visit project_pages_path(project)
+ it 'removes the pages' do
+ visit project_pages_path(project)
- expect(page).to have_link('Remove pages')
+ expect(page).to have_link('Remove pages')
- click_link 'Remove pages'
+ click_link 'Remove pages'
- expect(project.pages_deployed?).to be_falsey
- end
+ expect(project.pages_deployed?).to be_falsey
end
end
end
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
index 7cd5b12802b..74b9a2b20cd 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
@@ -6,11 +6,17 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' }
- def fill_form(active = true)
+ def fill_short_form(active = true)
check 'Active' if active
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"
+ end
+
+ def fill_full_form(active = true)
+ fill_short_form(active)
+ check 'Active' if active
+
fill_in 'service_new_issue_url', with: url
end
@@ -21,14 +27,20 @@ describe 'User activates issue tracker', :js do
visit project_settings_integrations_path(project)
end
- shared_examples 'external issue tracker activation' do |tracker:|
+ shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false|
describe 'user sets and activates the Service' do
context 'when the connection test succeeds' do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
click_link(tracker)
- fill_form
+
+ if skip_new_issue_url
+ fill_short_form
+ else
+ fill_full_form
+ end
+
click_button('Test settings and save changes')
wait_for_requests
end
@@ -50,7 +62,13 @@ describe 'User activates issue tracker', :js do
stub_request(:head, url).to_raise(HTTParty::Error)
click_link(tracker)
- fill_form
+
+ if skip_new_issue_url
+ fill_short_form
+ else
+ fill_full_form
+ end
+
click_button('Test settings and save changes')
wait_for_requests
@@ -69,7 +87,13 @@ describe 'User activates issue tracker', :js do
describe 'user sets the service but keeps it disabled' do
before do
click_link(tracker)
- fill_form(false)
+
+ if skip_new_issue_url
+ fill_short_form(false)
+ else
+ fill_full_form(false)
+ end
+
click_button('Save changes')
end
@@ -87,6 +111,7 @@ describe 'User activates issue tracker', :js do
end
it_behaves_like 'external issue tracker activation', tracker: 'Redmine'
+ it_behaves_like 'external issue tracker activation', tracker: 'YouTrack', skip_new_issue_url: true
it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla'
it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker'
end
diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb
new file mode 100644
index 00000000000..bb6a030c1cf
--- /dev/null
+++ b/spec/features/projects/services/user_activates_youtrack_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'User activates issue tracker', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:url) { 'http://tracker.example.com' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_project_url', with: url
+ fill_in 'service_issues_url', with: "#{url}/:id"
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ visit project_settings_integrations_path(project)
+ end
+
+ shared_examples 'external issue tracker activation' do |tracker:|
+ describe 'user sets and activates the Service' do
+ context 'when the connection test succeeds' do
+ before do
+ stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
+
+ click_link(tracker)
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+ end
+
+ it 'activates the service' do
+ expect(page).to have_content("#{tracker} activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+
+ it 'shows the link in the menu' do
+ page.within('.nav-sidebar') do
+ expect(page).to have_link(tracker, href: url)
+ end
+ end
+ end
+
+ context 'when the connection test fails' do
+ it 'activates the service' do
+ stub_request(:head, url).to_raise(HTTParty::Error)
+
+ click_link(tracker)
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed.'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').click
+ wait_for_requests
+
+ expect(page).to have_content("#{tracker} activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+ end
+ end
+
+ describe 'user sets the service but keeps it disabled' do
+ before do
+ click_link(tracker)
+ fill_form(false)
+ click_button('Save changes')
+ end
+
+ it 'saves but does not activate the service' do
+ expect(page).to have_content("#{tracker} settings saved, but not activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+
+ it 'does not show the external tracker link in the menu' do
+ page.within('.nav-sidebar') do
+ expect(page).not_to have_link(tracker, href: url)
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'external issue tracker activation', tracker: 'YouTrack'
+end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index df33d215602..dc0278370aa 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -7,6 +7,7 @@ describe 'Projects > Settings > For a forked project', :js do
let(:forked_project) { fork_project(original_project, user) }
before do
+ stub_feature_flags(approval_rules: false)
original_project.add_maintainer(user)
forked_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb
index 676659b90c3..e5a58c44e41 100644
--- a/spec/features/projects/settings/user_manages_group_links_spec.rb
+++ b/spec/features/projects/settings/user_manages_group_links_spec.rb
@@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do
before do
project.add_maintainer(user)
+ group_market.add_guest(user)
sign_in(user)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index 3b469fee867..49058d1372a 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'Projects > Wiki > User previews markdown changes', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) }
let(:wiki_content) do
<<-HEREDOC
[regular link](regular)
@@ -18,9 +19,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
sign_in(user)
- visit project_path(project)
- find('.shortcuts-wiki').click
- click_link "Create your first page"
+ visit project_wiki_path(project, wiki_page)
end
context "while creating a new wiki page" do
@@ -171,7 +170,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
fill_in :wiki_content, with: "1. one\n - sublist\n"
click_on "Preview"
- # the above generates two seperate lists (not embedded) in CommonMark
+ # the above generates two separate lists (not embedded) in CommonMark
expect(page).to have_content("sublist")
expect(page).not_to have_xpath("//ol//li//ul")
end
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 48a0d675f2d..b1a7f167977 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -44,13 +44,7 @@ describe "User creates wiki page" do
end
it "shows non-escaped link in the pages list", :js do
- click_link("New page")
-
- page.within("#modal-new-wiki") do
- fill_in(:new_wiki_path, with: "one/two/three-test")
-
- click_on("Create page")
- end
+ fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
fill_in(:wiki_content, with: "wiki content")
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f76e577b0d6..dbf8af3e5bb 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -26,12 +26,7 @@ describe 'User updates wiki page' do
end
it 'updates a page that has a path', :js do
- click_on('New page')
-
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
- end
+ fill_in(:wiki_title, with: 'one/two/three-test')
page.within '.wiki-form' do
fill_in(:wiki_content, with: 'wiki content')
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index d4691b669c1..6e28ec0d7b2 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -22,12 +22,7 @@ describe 'User views a wiki page' do
visit(project_wikis_path(project))
click_link "Create your first page"
- click_on('New page')
-
- page.within('#modal-new-wiki') do
- fill_in(:new_wiki_path, with: 'one/two/three-test')
- click_on('Create page')
- end
+ fill_in(:wiki_title, with: 'one/two/three-test')
page.within('.wiki-form') do
fill_in(:wiki_content, with: 'wiki content')
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index bc36c6f948f..dbf0d427976 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -4,6 +4,10 @@ describe 'Project' do
include ProjectForksHelper
include MobileHelpers
+ before do
+ stub_feature_flags(approval_rules: false)
+ end
+
describe 'creating from template' do
let(:user) { create(:user) }
let(:template) { Gitlab::ProjectTemplate.find(:rails) }
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 4705cd12d23..3238e07fe15 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -27,7 +27,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -42,7 +42,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -58,7 +58,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -73,7 +73,7 @@ describe 'Private Group access' do
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
- it { is_expected.to be_allowed_for(project_guest) }
+ it { is_expected.to be_denied_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
@@ -93,4 +93,28 @@ describe 'Private Group access' do
it { is_expected.to be_denied_for(:visitor) }
it { is_expected.to be_denied_for(:external) }
end
+
+ describe 'GET /groups/:path for shared projects' do
+ let(:project) { create(:project, :public) }
+ before do
+ Projects::GroupLinks::CreateService.new(
+ project,
+ create(:user),
+ link_group_access: ProjectGroupLink::DEVELOPER
+ ).execute(group)
+ end
+
+ subject { group_path(group) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index bfe11ddf673..957c3cfc583 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -49,6 +49,34 @@ describe 'Signup' do
expect(page).to have_content("Please create a username with only alphanumeric characters.")
end
+
+ it 'shows an error border if the username contains emojis' do
+ simulate_input('#new_user_username', 'ehsan😀')
+
+ expect(find('.username')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error message if the username contains emojis' do
+ simulate_input('#new_user_username', 'ehsan😀')
+
+ expect(page).to have_content("Invalid input, please avoid emojis")
+ end
+ end
+
+ describe 'user\'s full name validation', :js do
+ before do
+ visit root_path
+ click_link 'Register'
+ simulate_input('#new_user_name', 'Ehsan 🦋')
+ end
+
+ it 'shows an error border if the user\'s fullname contains an emoji' do
+ expect(find('.name')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error message if the username contains emojis' do
+ expect(page).to have_content("Invalid input, please avoid emojis")
+ end
end
context 'with no errors' do
diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb
index a4ad331f613..e074e53c2c5 100644
--- a/spec/finders/concerns/finder_methods_spec.rb
+++ b/spec/finders/concerns/finder_methods_spec.rb
@@ -12,7 +12,7 @@ describe FinderMethods do
end
def execute
- Project.all
+ Project.all.order(id: :desc)
end
end
end
@@ -38,6 +38,16 @@ describe FinderMethods do
it 'raises not found the user does not have access' do
expect { finder.find_by!(id: unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
+
+ it 'ignores ordering' do
+ # Memoise the finder result so we can add message expectations to it
+ relation = finder.execute
+ allow(finder).to receive(:execute).and_return(relation)
+
+ expect(relation).to receive(:reorder).with(nil).and_call_original
+
+ finder.find_by!(id: authorized_project.id)
+ end
end
describe '#find' do
@@ -66,5 +76,15 @@ describe FinderMethods do
it 'returns nil when the user does not have access' do
expect(finder.find_by(id: unauthorized_project.id)).to be_nil
end
+
+ it 'ignores ordering' do
+ # Memoise the finder result so we can add message expectations to it
+ relation = finder.execute
+ allow(finder).to receive(:execute).and_return(relation)
+
+ expect(relation).to receive(:reorder).with(nil).and_call_original
+
+ finder.find_by(id: authorized_project.id)
+ end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 34cb09942be..47e2548c3d6 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -416,6 +416,36 @@ describe IssuesFinder do
end
end
+ context 'filtering by closed_at' do
+ let!(:closed_issue1) { create(:issue, project: project1, state: :closed, closed_at: 1.week.ago) }
+ let!(:closed_issue2) { create(:issue, project: project2, state: :closed, closed_at: 1.week.from_now) }
+ let!(:closed_issue3) { create(:issue, project: project2, state: :closed, closed_at: 2.weeks.from_now) }
+
+ context 'through closed_after' do
+ let(:params) { { state: :closed, closed_after: closed_issue3.closed_at } }
+
+ it 'returns issues closed on or after the given date' do
+ expect(issues).to contain_exactly(closed_issue3)
+ end
+ end
+
+ context 'through closed_before' do
+ let(:params) { { state: :closed, closed_before: closed_issue1.closed_at } }
+
+ it 'returns issues closed on or before the given date' do
+ expect(issues).to contain_exactly(closed_issue1)
+ end
+ end
+
+ context 'through closed_after and closed_before' do
+ let(:params) { { state: :closed, closed_after: closed_issue2.closed_at, closed_before: closed_issue3.closed_at } }
+
+ it 'returns issues closed between the given dates' do
+ expect(issues).to contain_exactly(closed_issue2, closed_issue3)
+ end
+ end
+ end
+
context 'filtering by reaction name' do
context 'user searches by no reaction' do
let(:params) { { my_reaction_emoji: 'None' } }
@@ -460,6 +490,32 @@ describe IssuesFinder do
end
end
+ context 'filtering by confidential' do
+ set(:confidential_issue) { create(:issue, project: project1, confidential: true) }
+
+ context 'no filtering' do
+ it 'returns all issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, confidential_issue)
+ end
+ end
+
+ context 'user filters confidential issues' do
+ let(:params) { { confidential: true } }
+
+ it 'returns only confdential issues' do
+ expect(issues).to contain_exactly(confidential_issue)
+ end
+ end
+
+ context 'user filters only public issues' do
+ let(:params) { { confidential: false } }
+
+ it 'returns only confdential issues' do
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
+ end
+ end
+ end
+
context 'when the user is unauthorized' do
let(:search_user) { nil }
@@ -526,7 +582,7 @@ describe IssuesFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(4)
+ expect(finder.row_count).to eq(5)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 107da08a0a9..79f854cdb96 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequestsFinder do
p
end
end
- let(:project4) { create_project_without_n_plus_1(group: subgroup) }
+ let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) }
let(:project5) { create_project_without_n_plus_1(group: subgroup) }
let(:project6) { create_project_without_n_plus_1(group: subgroup) }
@@ -68,6 +68,15 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(2)
end
+ it 'filters by commit sha' do
+ merge_requests = described_class.new(
+ user,
+ commit_sha: merge_request5.merge_request_diff.last_commit_sha
+ ).execute
+
+ expect(merge_requests).to contain_exactly(merge_request5)
+ end
+
context 'filtering by group' do
it 'includes all merge requests when user has access' do
params = { group_id: group.id }
@@ -269,6 +278,21 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
end
end
+
+ context 'when project restricts merge requests' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+
+ it "returns nothing to to non members" do
+ merge_requests = described_class.new(
+ non_member,
+ project_id: project.id
+ ).execute
+
+ expect(merge_requests).to be_empty
+ end
+ end
end
describe '#row_count', :request_store do
diff --git a/spec/fixtures/api/schemas/entities/diff_viewer.json b/spec/fixtures/api/schemas/entities/diff_viewer.json
index 81325cd86c6..ae0fb32d3ac 100644
--- a/spec/fixtures/api/schemas/entities/diff_viewer.json
+++ b/spec/fixtures/api/schemas/entities/diff_viewer.json
@@ -14,6 +14,17 @@
"string",
"null"
]
+ },
+ "error_message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "collapsed": {
+ "type": [
+ "boolean"
+ ]
}
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json
index f1d33e3ce7b..9a10ab18c30 100644
--- a/spec/fixtures/api/schemas/environment.json
+++ b/spec/fixtures/api/schemas/environment.json
@@ -20,6 +20,7 @@
"state": { "type": "string" },
"external_url": { "$ref": "types/nullable_string.json" },
"environment_type": { "$ref": "types/nullable_string.json" },
+ "name_without_type": { "type": "string" },
"has_stop_action": { "type": "boolean" },
"environment_path": { "type": "string" },
"stop_path": { "type": "string" },
diff --git a/spec/fixtures/api/schemas/public_api/v4/milestone.json b/spec/fixtures/api/schemas/public_api/v4/milestone.json
index 971f7980f46..6ca2e88ae91 100644
--- a/spec/fixtures/api/schemas/public_api/v4/milestone.json
+++ b/spec/fixtures/api/schemas/public_api/v4/milestone.json
@@ -8,7 +8,6 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
- "percentage_complete": { "type": "integer" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"start_date": { "type": "date" },
diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json
index 68c6099836b..03dfc647162 100644
--- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json
+++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json
@@ -1,11 +1,14 @@
{
"image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff",
"unapproved": [
- "CVE-2017-18018",
- "CVE-2016-2781",
- "CVE-2017-12424",
- "CVE-2007-5686",
- "CVE-2013-4235"
+ "CVE-2017-18269",
+ "CVE-2017-16997",
+ "CVE-2018-1000001",
+ "CVE-2016-10228",
+ "CVE-2018-18520",
+ "CVE-2010-4052",
+ "CVE-2018-16869",
+ "CVE-2018-18311"
],
"vulnerabilities": [
{
@@ -87,6 +90,16 @@
"link": "https://security-tracker.debian.org/tracker/CVE-2018-18311",
"severity": "Unknown",
"fixedby": "5.24.1-3+deb9u5"
+ },
+ {
+ "featurename": "foo",
+ "featureversion": "1.3",
+ "vulnerability": "CVE-2018-666",
+ "namespace": "debian:9",
+ "description": "Foo has a vulnerability nobody cares about and whitelist.",
+ "link": "https://security-tracker.debian.org/tracker/CVE-2018-666",
+ "severity": "Unknown",
+ "fixedby": "1.4"
}
]
}
diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js
new file mode 100644
index 00000000000..08d725cd4e4
--- /dev/null
+++ b/spec/frontend/__mocks__/file_mock.js
@@ -0,0 +1 @@
+export default '';
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index a14031f43ed..c7008c780d6 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -6,58 +6,62 @@ import GfmAutoComplete from '~/gfm_auto_complete';
import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
-describe('GfmAutoComplete', function() {
+describe('GfmAutoComplete', () => {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
fetchData: () => {},
});
- describe('DefaultOptions.sorter', function() {
- describe('assets loading', function() {
- beforeEach(function() {
- spyOn(GfmAutoComplete, 'isLoading').and.returnValue(true);
+ let atwhoInstance;
+ let items;
+ let sorterValue;
- this.atwhoInstance = { setting: {} };
- this.items = [];
+ describe('DefaultOptions.sorter', () => {
+ describe('assets loading', () => {
+ beforeEach(() => {
+ jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true);
- this.sorterValue = gfmAutoCompleteCallbacks.sorter.call(this.atwhoInstance, '', this.items);
+ atwhoInstance = { setting: {} };
+ items = [];
+
+ sorterValue = gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, '', items);
});
- it('should disable highlightFirst', function() {
- expect(this.atwhoInstance.setting.highlightFirst).toBe(false);
+ it('should disable highlightFirst', () => {
+ expect(atwhoInstance.setting.highlightFirst).toBe(false);
});
- it('should return the passed unfiltered items', function() {
- expect(this.sorterValue).toEqual(this.items);
+ it('should return the passed unfiltered items', () => {
+ expect(sorterValue).toEqual(items);
});
});
- describe('assets finished loading', function() {
- beforeEach(function() {
- spyOn(GfmAutoComplete, 'isLoading').and.returnValue(false);
- spyOn($.fn.atwho.default.callbacks, 'sorter');
+ describe('assets finished loading', () => {
+ beforeEach(() => {
+ jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(false);
+ jest.spyOn($.fn.atwho.default.callbacks, 'sorter').mockImplementation(() => {});
});
- it('should enable highlightFirst if alwaysHighlightFirst is set', function() {
- const atwhoInstance = { setting: { alwaysHighlightFirst: true } };
+ it('should enable highlightFirst if alwaysHighlightFirst is set', () => {
+ atwhoInstance = { setting: { alwaysHighlightFirst: true } };
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance);
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
- it('should enable highlightFirst if a query is present', function() {
- const atwhoInstance = { setting: {} };
+ it('should enable highlightFirst if a query is present', () => {
+ atwhoInstance = { setting: {} };
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query');
expect(atwhoInstance.setting.highlightFirst).toBe(true);
});
- it('should call the default atwho sorter', function() {
- const atwhoInstance = { setting: {} };
+ it('should call the default atwho sorter', () => {
+ atwhoInstance = { setting: {} };
const query = 'query';
- const items = [];
+ items = [];
const searchKey = 'searchKey';
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey);
@@ -71,7 +75,9 @@ describe('GfmAutoComplete', function() {
const beforeInsert = (context, value) =>
gfmAutoCompleteCallbacks.beforeInsert.call(context, value);
- const atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
+ beforeEach(() => {
+ atwhoInstance = { setting: { skipSpecialCharacterTest: false } };
+ });
it('should not quote if value only contains alphanumeric charecters', () => {
expect(beforeInsert(atwhoInstance, '@user1')).toBe('@user1');
@@ -96,7 +102,7 @@ describe('GfmAutoComplete', function() {
});
});
- describe('DefaultOptions.matcher', function() {
+ describe('DefaultOptions.matcher', () => {
const defaultMatcher = (context, flag, subtext) =>
gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext);
@@ -108,7 +114,10 @@ describe('GfmAutoComplete', function() {
hash[el] = null;
return hash;
}, {});
- const atwhoInstance = { setting: {}, app: { controllers: flagsHash } };
+
+ beforeEach(() => {
+ atwhoInstance = { setting: {}, app: { controllers: flagsHash } };
+ });
const minLen = 1;
const maxLen = 20;
@@ -182,38 +191,38 @@ describe('GfmAutoComplete', function() {
});
});
- describe('isLoading', function() {
- it('should be true with loading data object item', function() {
+ describe('isLoading', () => {
+ it('should be true with loading data object item', () => {
expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
});
- it('should be true with loading data array', function() {
+ it('should be true with loading data array', () => {
expect(GfmAutoComplete.isLoading(['loading'])).toBe(true);
});
- it('should be true with loading data object array', function() {
+ it('should be true with loading data object array', () => {
expect(GfmAutoComplete.isLoading([{ name: 'loading' }])).toBe(true);
});
- it('should be false with actual array data', function() {
+ it('should be false with actual array data', () => {
expect(
GfmAutoComplete.isLoading([{ title: 'Foo' }, { title: 'Bar' }, { title: 'Qux' }]),
).toBe(false);
});
- it('should be false with actual data item', function() {
+ it('should be false with actual data item', () => {
expect(GfmAutoComplete.isLoading({ title: 'Foo' })).toBe(false);
});
});
- describe('Issues.insertTemplateFunction', function() {
- it('should return default template', function() {
+ describe('Issues.insertTemplateFunction', () => {
+ it('should return default template', () => {
expect(GfmAutoComplete.Issues.insertTemplateFunction({ id: 5, title: 'Some Issue' })).toBe(
'${atwho-at}${id}', // eslint-disable-line no-template-curly-in-string
);
});
- it('should return reference when reference is set', function() {
+ it('should return reference when reference is set', () => {
expect(
GfmAutoComplete.Issues.insertTemplateFunction({
id: 5,
@@ -224,14 +233,14 @@ describe('GfmAutoComplete', function() {
});
});
- describe('Issues.templateFunction', function() {
- it('should return html with id and title', function() {
+ describe('Issues.templateFunction', () => {
+ it('should return html with id and title', () => {
expect(GfmAutoComplete.Issues.templateFunction({ id: 5, title: 'Some Issue' })).toBe(
'<li><small>5</small> Some Issue</li>',
);
});
- it('should replace id with reference if reference is set', function() {
+ it('should replace id with reference if reference is set', () => {
expect(
GfmAutoComplete.Issues.templateFunction({
id: 5,
diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js
index 7bb8e26b81a..7bb8e26b81a 100644
--- a/spec/javascripts/issuable_suggestions/components/app_spec.js
+++ b/spec/frontend/issuable_suggestions/components/app_spec.js
diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 7bd1fe678f4..7bd1fe678f4 100644
--- a/spec/javascripts/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/frontend/issuable_suggestions/mock_data.js
index 4f0f9ef8d62..4f0f9ef8d62 100644
--- a/spec/javascripts/issuable_suggestions/mock_data.js
+++ b/spec/frontend/issuable_suggestions/mock_data.js
diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js
index dc0b04173bf..e2ee70b9d69 100644
--- a/spec/javascripts/lib/utils/ajax_cache_spec.js
+++ b/spec/frontend/lib/utils/ajax_cache_spec.js
@@ -94,68 +94,54 @@ describe('AjaxCache', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- spyOn(axios, 'get').and.callThrough();
+ jest.spyOn(axios, 'get');
});
afterEach(() => {
mock.restore();
});
- it('stores and returns data from Ajax call if cache is empty', done => {
+ it('stores and returns data from Ajax call if cache is empty', () => {
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
- AjaxCache.retrieve(dummyEndpoint)
- .then(data => {
- expect(data).toEqual(dummyResponse);
- expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
- })
- .then(done)
- .catch(fail);
+ return AjaxCache.retrieve(dummyEndpoint).then(data => {
+ expect(data).toEqual(dummyResponse);
+ expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
+ });
});
- it('makes no Ajax call if request is pending', done => {
+ it('makes no Ajax call if request is pending', () => {
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
- AjaxCache.retrieve(dummyEndpoint)
- .then(done)
- .catch(fail);
-
- AjaxCache.retrieve(dummyEndpoint)
- .then(done)
- .catch(fail);
-
- expect(axios.get.calls.count()).toBe(1);
+ return Promise.all([
+ AjaxCache.retrieve(dummyEndpoint),
+ AjaxCache.retrieve(dummyEndpoint),
+ ]).then(() => {
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ });
});
- it('returns undefined if Ajax call fails and cache is empty', done => {
+ it('returns undefined if Ajax call fails and cache is empty', () => {
const errorMessage = 'Network Error';
mock.onGet(dummyEndpoint).networkError();
- AjaxCache.retrieve(dummyEndpoint)
- .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
- .catch(error => {
- expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
- expect(error.textStatus).toBe(errorMessage);
- done();
- })
- .catch(fail);
+ expect.assertions(2);
+ return AjaxCache.retrieve(dummyEndpoint).catch(error => {
+ expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
+ expect(error.textStatus).toBe(errorMessage);
+ });
});
- it('makes no Ajax call if matching data exists', done => {
+ it('makes no Ajax call if matching data exists', () => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
- mock.onGet(dummyEndpoint).reply(() => {
- fail(new Error('expected no Ajax call!'));
- });
- AjaxCache.retrieve(dummyEndpoint)
- .then(data => {
- expect(data).toBe(dummyResponse);
- })
- .then(done)
- .catch(fail);
+ return AjaxCache.retrieve(dummyEndpoint).then(data => {
+ expect(data).toBe(dummyResponse);
+ expect(axios.get).not.toHaveBeenCalled();
+ });
});
- it('makes Ajax call even if matching data exists when forceRequest parameter is provided', done => {
+ it('makes Ajax call even if matching data exists when forceRequest parameter is provided', () => {
const oldDummyResponse = {
important: 'old dummy data',
};
@@ -164,21 +150,12 @@ describe('AjaxCache', () => {
mock.onGet(dummyEndpoint).reply(200, dummyResponse);
- // Call without forceRetrieve param
- AjaxCache.retrieve(dummyEndpoint)
- .then(data => {
- expect(data).toBe(oldDummyResponse);
- })
- .then(done)
- .catch(fail);
-
- // Call with forceRetrieve param
- AjaxCache.retrieve(dummyEndpoint, true)
- .then(data => {
- expect(data).toEqual(dummyResponse);
- })
- .then(done)
- .catch(fail);
+ return Promise.all([
+ AjaxCache.retrieve(dummyEndpoint),
+ AjaxCache.retrieve(dummyEndpoint, true),
+ ]).then(data => {
+ expect(data).toEqual([oldDummyResponse, dummyResponse]);
+ });
});
});
});
diff --git a/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
new file mode 100644
index 00000000000..11d65ced180
--- /dev/null
+++ b/spec/frontend/notes/components/__snapshots__/discussion_jump_to_next_button_spec.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`JumpToNextDiscussionButton matches the snapshot 1`] = `
+<div
+ class="btn-group"
+ role="group"
+>
+ <button
+ class="btn btn-default discussion-next-btn"
+ data-original-title="Jump to next unresolved discussion"
+ title=""
+ >
+ <icon-stub
+ cssclasses=""
+ name="comment-next"
+ size="16"
+ />
+ </button>
+</div>
+`;
diff --git a/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
new file mode 100644
index 00000000000..989b0458481
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_jump_to_next_button_spec.js
@@ -0,0 +1,30 @@
+import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
+import { shallowMount } from '@vue/test-utils';
+
+describe('JumpToNextDiscussionButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(JumpToNextDiscussionButton, {
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.vm.$el).toMatchSnapshot();
+ });
+
+ it('emits onClick event on button click', () => {
+ const button = wrapper.find({ ref: 'button' });
+
+ button.trigger('click');
+
+ expect(wrapper.emitted()).toEqual({
+ onClick: [[]],
+ });
+ });
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 7ad2e97e7e6..d892889b98d 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,3 +1,7 @@
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import axios from '~/lib/utils/axios_utils';
+
const testTimeoutInMs = 300;
jest.setTimeout(testTimeoutInMs);
@@ -14,3 +18,17 @@ afterEach(() => {
throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
}
});
+
+// fail tests for unmocked requests
+beforeEach(done => {
+ axios.defaults.adapter = config => {
+ const error = new Error(`Unexpected unmocked request: ${JSON.stringify(config, null, 2)}`);
+ error.config = config;
+ done.fail(error);
+ return Promise.reject(error);
+ };
+
+ done();
+});
+
+Vue.use(Translate);
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
new file mode 100644
index 00000000000..a229d29afa6
--- /dev/null
+++ b/spec/graphql/features/authorization_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Gitlab::Graphql::Authorization' do
+ set(:user) { create(:user) }
+
+ let(:test_object) { double(name: 'My name') }
+ let(:object_type) { object_type_class }
+ let(:query_type) { query_type_class(object_type, test_object) }
+ let(:schema) { schema_class(query_type) }
+
+ let(:execute) do
+ schema.execute(
+ query_string,
+ context: { current_user: user },
+ variables: {}
+ )
+ end
+
+ let(:result) { execute['data'] }
+
+ before do
+ # By default, disallow all permissions.
+ allow(Ability).to receive(:allowed?).and_return(false)
+ end
+
+ describe 'authorizing with a single permission' do
+ let(:query_string) { '{ singlePermission() { name } }' }
+
+ subject { result['singlePermission'] }
+
+ it 'should return the protected field when user has permission' do
+ permit(:foo)
+
+ expect(subject['name']).to eq(test_object.name)
+ end
+
+ it 'should return nil when user is not authorized' do
+ expect(subject).to be_nil
+ end
+ end
+
+ describe 'authorizing with an Array of permissions' do
+ let(:query_string) { '{ permissionCollection() { name } }' }
+
+ subject { result['permissionCollection'] }
+
+ it 'should return the protected field when user has all permissions' do
+ permit(:foo, :bar)
+
+ expect(subject['name']).to eq(test_object.name)
+ end
+
+ it 'should return nil when user only has one of the permissions' do
+ permit(:foo)
+
+ expect(subject).to be_nil
+ end
+
+ it 'should return nil when user only has none of the permissions' do
+ expect(subject).to be_nil
+ end
+ end
+
+ private
+
+ def permit(*permissions)
+ permissions.each do |permission|
+ allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
+ end
+ end
+
+ def object_type_class
+ Class.new(Types::BaseObject) do
+ graphql_name 'TestObject'
+
+ field :name, GraphQL::STRING_TYPE, null: true
+ end
+ end
+
+ def query_type_class(type, object)
+ Class.new(Types::BaseObject) do
+ graphql_name 'TestQuery'
+
+ field :single_permission, type,
+ null: true,
+ authorize: :foo,
+ resolve: ->(obj, args, ctx) { object }
+
+ field :permission_collection, type,
+ null: true,
+ resolve: ->(obj, args, ctx) { object } do
+ authorize [:foo, :bar]
+ end
+ end
+ end
+
+ def schema_class(query)
+ Class.new(GraphQL::Schema) do
+ use Gitlab::Graphql::Authorize
+
+ query(query)
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
new file mode 100644
index 00000000000..e3a34762b62
--- /dev/null
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::BaseResolver do
+ include GraphqlHelpers
+
+ let(:resolver) do
+ Class.new(described_class) do
+ def resolve(**args)
+ [args, args]
+ end
+ end
+ end
+
+ describe '.single' do
+ it 'returns a subclass from the resolver' do
+ expect(resolver.single.superclass).to eq(resolver)
+ end
+
+ it 'returns the same subclass every time' do
+ expect(resolver.single.object_id).to eq(resolver.single.object_id)
+ end
+
+ it 'returns a resolver that gives the first result from the original resolver' do
+ result = resolve(resolver.single, args: { test: 1 })
+
+ expect(result).to eq(test: 1)
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 1a54ab540fc..5f9c180cbb7 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -5,16 +5,63 @@ describe Resolvers::IssuesResolver do
let(:current_user) { create(:user) }
set(:project) { create(:project) }
- set(:issue) { create(:issue, project: project) }
- set(:issue2) { create(:issue, project: project, title: 'foo') }
+ set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
+ set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
+ set(:label1) { create(:label, project: project) }
+ set(:label2) { create(:label, project: project) }
before do
project.add_developer(current_user)
+ create(:label_link, label: label1, target: issue1)
+ create(:label_link, label: label1, target: issue2)
+ create(:label_link, label: label2, target: issue2)
end
describe '#resolve' do
it 'finds all issues' do
- expect(resolve_issues).to contain_exactly(issue, issue2)
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
+
+ it 'filters by state' do
+ expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
+ expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
+ end
+
+ it 'filters by labels' do
+ expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
+ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
+ end
+
+ describe 'filters by created_at' do
+ it 'filters by created_before' do
+ expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by created_after' do
+ expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
+ end
+
+ describe 'filters by updated_at' do
+ it 'filters by updated_before' do
+ expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by updated_after' do
+ expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
+ end
+
+ describe 'filters by closed_at' do
+ let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+
+ it 'filters by closed_before' do
+ expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ end
+
+ it 'filters by closed_after' do
+ expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
it 'searches issues' do
@@ -22,7 +69,7 @@ describe Resolvers::IssuesResolver do
end
it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue]
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
end
it 'returns issues user can see' do
@@ -30,27 +77,31 @@ describe Resolvers::IssuesResolver do
create(:issue, confidential: true)
- expect(resolve_issues).to contain_exactly(issue, issue2)
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
+
+ it 'finds a specific issue with iid' do
+ expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
end
it 'finds a specific issue with iids' do
- expect(resolve_issues(iids: issue.iid)).to contain_exactly(issue)
+ expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
end
it 'finds multiple issues with iids' do
- expect(resolve_issues(iids: [issue.iid, issue2.iid]))
- .to contain_exactly(issue, issue2)
+ expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
+ .to contain_exactly(issue1, issue2)
end
it 'finds only the issues within the project we are looking at' do
another_project = create(:project)
- iids = [issue, issue2].map(&:iid)
+ iids = [issue1, issue2].map(&:iid)
iids.each do |iid|
create(:issue, project: another_project, iid: iid)
end
- expect(resolve_issues(iids: iids)).to contain_exactly(issue, issue2)
+ expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
end
end
diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
index 73993b3a039..ab3c426b2cd 100644
--- a/spec/graphql/resolvers/merge_request_resolver_spec.rb
+++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Resolvers::MergeRequestResolver do
+describe Resolvers::MergeRequestsResolver do
include GraphqlHelpers
set(:project) { create(:project, :repository) }
@@ -16,9 +16,17 @@ describe Resolvers::MergeRequestResolver do
let(:other_iid) { other_merge_request.iid }
describe '#resolve' do
- it 'batch-resolves merge requests by target project full path and IID' do
+ it 'batch-resolves by target project full path and individual IID' do
result = batch(max_queries: 2) do
- [resolve_mr(project, iid_1), resolve_mr(project, iid_2)]
+ resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2)
+ end
+
+ expect(result).to contain_exactly(merge_request_1, merge_request_2)
+ end
+
+ it 'batch-resolves by target project full path and IIDS' do
+ result = batch(max_queries: 2) do
+ resolve_mr(project, iids: [iid_1, iid_2])
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
@@ -26,20 +34,28 @@ describe Resolvers::MergeRequestResolver do
it 'can batch-resolve merge requests from different projects' do
result = batch(max_queries: 3) do
- [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)]
+ resolve_mr(project, iid: iid_1) +
+ resolve_mr(project, iid: iid_2) +
+ resolve_mr(other_project, iid: other_iid)
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request)
end
- it 'resolves an unknown iid to nil' do
- result = batch { resolve_mr(project, -1) }
+ it 'resolves an unknown iid to be empty' do
+ result = batch { resolve_mr(project, iid: -1) }
+
+ expect(result).to be_empty
+ end
+
+ it 'resolves empty iids to be empty' do
+ result = batch { resolve_mr(project, iids: []) }
- expect(result).to be_nil
+ expect(result).to be_empty
end
end
- def resolve_mr(project, iid)
- resolve(described_class, obj: project, args: { iid: iid })
+ def resolve_mr(project, args)
+ resolve(described_class, obj: project, args: args)
end
end
diff --git a/spec/graphql/types/issuable_state_enum_spec.rb b/spec/graphql/types/issuable_state_enum_spec.rb
new file mode 100644
index 00000000000..65a80fa4176
--- /dev/null
+++ b/spec/graphql/types/issuable_state_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['IssuableState'] do
+ it { expect(described_class.graphql_name).to eq('IssuableState') }
+
+ it_behaves_like 'issuable state'
+end
diff --git a/spec/graphql/types/issue_state_enum_spec.rb b/spec/graphql/types/issue_state_enum_spec.rb
new file mode 100644
index 00000000000..de19e6fc505
--- /dev/null
+++ b/spec/graphql/types/issue_state_enum_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['IssueState'] do
+ it { expect(described_class.graphql_name).to eq('IssueState') }
+
+ it_behaves_like 'issuable state'
+end
diff --git a/spec/graphql/types/merge_request_state_enum_spec.rb b/spec/graphql/types/merge_request_state_enum_spec.rb
new file mode 100644
index 00000000000..626e33b18d3
--- /dev/null
+++ b/spec/graphql/types/merge_request_state_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['MergeRequestState'] do
+ it { expect(described_class.graphql_name).to eq('MergeRequestState') }
+
+ it_behaves_like 'issuable state'
+
+ it 'exposes all the existing merge request states' do
+ expect(described_class.values.keys).to include('merged')
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 01d71abfac9..e8f1c84f8d6 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -6,12 +6,18 @@ describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') }
describe 'nested merge request' do
+ it { expect(described_class).to have_graphql_field(:merge_requests) }
it { expect(described_class).to have_graphql_field(:merge_request) }
it 'authorizes the merge request' do
expect(described_class.fields['mergeRequest'])
.to require_graphql_authorizations(:read_merge_request)
end
+
+ it 'authorizes the merge requests' do
+ expect(described_class.fields['mergeRequests'])
+ .to require_graphql_authorizations(:read_merge_request)
+ end
end
describe 'nested issues' do
diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb
new file mode 100644
index 00000000000..8d717b968dd
--- /dev/null
+++ b/spec/helpers/appearances_helper_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AppearancesHelper do
+ before do
+ user = create(:user)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ describe '#header_message' do
+ it 'returns nil when header message field is not set' do
+ create(:appearance)
+
+ expect(helper.header_message).to be_nil
+ end
+
+ context 'when header message is set' do
+ it 'includes current message' do
+ message = "Foo bar"
+ create(:appearance, header_message: message)
+
+ expect(helper.header_message).to include(message)
+ end
+ end
+ end
+
+ describe '#footer_message' do
+ it 'returns nil when footer message field is not set' do
+ create(:appearance)
+
+ expect(helper.footer_message).to be_nil
+ end
+
+ context 'when footer message is set' do
+ it 'includes current message' do
+ message = "Foo bar"
+ create(:appearance, footer_message: message)
+
+ expect(helper.footer_message).to include(message)
+ end
+ end
+ end
+
+ describe '#brand_image' do
+ let!(:appearance) { create(:appearance, :with_logo) }
+
+ context 'when there is a logo' do
+ it 'returns a path' do
+ expect(helper.brand_image).to match(%r(img data-src="/uploads/-/system/appearance/.*png))
+ end
+ end
+
+ context 'when there is a logo but no associated upload' do
+ before do
+ # Legacy attachments were not tracked in the uploads table
+ appearance.logo.upload.destroy
+ appearance.reload
+ end
+
+ it 'falls back to using the original path' do
+ expect(helper.brand_image).to match(%r(img data-src="/uploads/-/system/appearance/.*png))
+ end
+ end
+ end
+
+ describe '#brand_title' do
+ it 'returns the default CE title when no appearance is present' do
+ allow(helper)
+ .to receive(:current_appearance)
+ .and_return(nil)
+
+ expect(helper.brand_title).to eq('GitLab Community Edition')
+ end
+ end
+end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index af4931e3370..6e8c13db9fe 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -39,59 +39,12 @@ describe ImportHelper do
end
end
- describe '#provider_project_link' do
- context 'when provider is "github"' do
- let(:github_server_url) { nil }
- let(:provider) { OpenStruct.new(name: 'github', url: github_server_url) }
+ describe '#provider_project_link_url' do
+ let(:full_path) { '/repo/path' }
+ let(:host_url) { 'http://provider.com/' }
- before do
- stub_omniauth_setting(providers: [provider])
- end
-
- context 'when provider does not specify a custom URL' do
- it 'uses default GitHub URL' do
- expect(helper.provider_project_link('github', 'octocat/Hello-World'))
- .to include('href="https://github.com/octocat/Hello-World"')
- end
- end
-
- context 'when provider specify a custom URL' do
- let(:github_server_url) { 'https://github.company.com' }
-
- it 'uses custom URL' do
- expect(helper.provider_project_link('github', 'octocat/Hello-World'))
- .to include('href="https://github.company.com/octocat/Hello-World"')
- end
- end
-
- context "when custom URL contains a '/' char at the end" do
- let(:github_server_url) { 'https://github.company.com/' }
-
- it "doesn't render double slash" do
- expect(helper.provider_project_link('github', 'octocat/Hello-World'))
- .to include('href="https://github.company.com/octocat/Hello-World"')
- end
- end
-
- context 'when provider is missing' do
- it 'uses the default URL' do
- allow(Gitlab.config.omniauth).to receive(:providers).and_return([])
-
- expect(helper.provider_project_link('github', 'octocat/Hello-World'))
- .to include('href="https://github.com/octocat/Hello-World"')
- end
- end
- end
-
- context 'when provider is "gitea"' do
- before do
- assign(:gitea_host_url, 'https://try.gitea.io/')
- end
-
- it 'uses given host' do
- expect(helper.provider_project_link('gitea', 'octocat/Hello-World'))
- .to include('href="https://try.gitea.io/octocat/Hello-World"')
- end
+ it 'appends repo full path to provider host url' do
+ expect(helper.provider_project_link_url(host_url, full_path)).to match('http://provider.com/repo/path')
end
end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index c04f679bcf0..012678db9c2 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -236,4 +236,17 @@ describe LabelsHelper do
expect(labels_filter_path(format: :json)).to eq(dashboard_labels_path(format: :json))
end
end
+
+ describe 'labels_sorted_by_title' do
+ it 'sorts labels alphabetically' do
+ label1 = double(:label, title: 'a')
+ label2 = double(:label, title: 'B')
+ label3 = double(:label, title: 'c')
+ label4 = double(:label, title: 'D')
+ labels = [label1, label2, label3, label4]
+
+ expect(labels_sorted_by_title(labels))
+ .to match_array([label2, label4, label1, label3])
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 4c395248644..e0e8ebd0c3c 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -110,6 +110,13 @@ describe PreferencesHelper do
end
end
+ describe '#language_choices' do
+ it 'returns an array of all available languages' do
+ expect(helper.language_choices).to be_an(Array)
+ expect(helper.language_choices.map(&:second)).to eq(Gitlab::I18n.available_locales)
+ end
+ end
+
def stub_user(messages = {})
if messages.empty?
allow(helper).to receive(:current_user).and_return(nil)
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index a2cbc0f3c72..5abdfe695d0 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -68,6 +68,32 @@ describe('diffs/components/app', () => {
});
});
+ describe('resizable', () => {
+ afterEach(() => {
+ localStorage.removeItem('mr_tree_list_width');
+ });
+
+ it('sets initial width when no localStorage has been set', () => {
+ createComponent();
+
+ expect(vm.vm.treeWidth).toEqual(320);
+ });
+
+ it('sets initial width to localStorage size', () => {
+ localStorage.setItem('mr_tree_list_width', '200');
+
+ createComponent();
+
+ expect(vm.vm.treeWidth).toEqual(200);
+ });
+
+ it('sets width of tree list', () => {
+ createComponent();
+
+ expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px');
+ });
+ });
+
describe('empty state', () => {
it('renders empty state when no diff files exist', () => {
createComponent();
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index 9e158327a77..a1bb51963d6 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -6,6 +6,7 @@ import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import '~/behaviors/markdown/render_gfm';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
+import { diffViewerModes } from '~/ide/constants';
describe('DiffContent', () => {
const Component = Vue.extend(DiffContentComponent);
@@ -52,26 +53,39 @@ describe('DiffContent', () => {
describe('empty files', () => {
beforeEach(() => {
- vm.diffFile.empty = true;
vm.diffFile.highlighted_diff_lines = [];
vm.diffFile.parallel_diff_lines = [];
});
- it('should render a message', done => {
+ it('should render a no preview message if viewer returns no preview', done => {
+ vm.diffFile.viewer.name = diffViewerModes.no_preview;
vm.$nextTick(() => {
const block = vm.$el.querySelector('.diff-viewer .nothing-here-block');
expect(block).not.toBe(null);
- expect(block.textContent.trim()).toContain('Empty file');
+ expect(block.textContent.trim()).toContain('No preview for this file type');
+
+ done();
+ });
+ });
+
+ it('should render a not diffable message if viewer returns not diffable', done => {
+ vm.diffFile.viewer.name = diffViewerModes.not_diffable;
+ vm.$nextTick(() => {
+ const block = vm.$el.querySelector('.diff-viewer .nothing-here-block');
+
+ expect(block).not.toBe(null);
+ expect(block.textContent.trim()).toContain(
+ 'This diff was suppressed by a .gitattributes entry',
+ );
done();
});
});
it('should not render multiple messages', done => {
- vm.diffFile.mode_changed = true;
vm.diffFile.b_mode = '100755';
- vm.diffFile.viewer.name = 'mode_changed';
+ vm.diffFile.viewer.name = diffViewerModes.mode_changed;
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.nothing-here-block').length).toBe(1);
@@ -81,6 +95,7 @@ describe('DiffContent', () => {
});
it('should not render diff table', done => {
+ vm.diffFile.viewer.name = diffViewerModes.no_preview;
vm.$nextTick(() => {
expect(vm.$el.querySelector('table')).toBe(null);
@@ -157,6 +172,7 @@ describe('DiffContent', () => {
vm.diffFile.new_sha = 'DEF';
vm.diffFile.old_path = 'test.abc';
vm.diffFile.old_sha = 'ABC';
+ vm.diffFile.viewer.name = diffViewerModes.added;
vm.$nextTick(() => {
expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 787a81fd88f..005a4751ea1 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -4,15 +4,15 @@ import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import diffDiscussionsMockData from '../mock_data/diff_discussions';
+import { diffViewerModes } from '~/ide/constants';
Vue.use(Vuex);
-const discussionFixture = 'merge_requests/diff_discussion.json';
-
describe('diff_file_header', () => {
let vm;
let props;
- const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
+ const diffDiscussionMock = diffDiscussionsMockData;
const Component = Vue.extend(DiffFileHeader);
const store = new Vuex.Store({
@@ -303,13 +303,13 @@ describe('diff_file_header', () => {
});
it('displays old and new path if the file was renamed', () => {
- props.diffFile.renamed_file = true;
+ props.diffFile.viewer.name = diffViewerModes.renamed;
vm = mountComponentWithStore(Component, { props, store });
expect(filePaths()).toHaveLength(2);
- expect(filePaths()[0]).toHaveText(props.diffFile.old_path);
- expect(filePaths()[1]).toHaveText(props.diffFile.new_path);
+ expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html);
+ expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html);
});
});
@@ -319,14 +319,12 @@ describe('diff_file_header', () => {
const button = vm.$el.querySelector('.btn-clipboard');
expect(button).not.toBe(null);
- expect(button.dataset.clipboardText).toBe(
- '{"text":"files/ruby/popen.rb","gfm":"`files/ruby/popen.rb`"}',
- );
+ expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}');
});
describe('file mode', () => {
it('it displays old and new file mode if it changed', () => {
- props.diffFile.mode_changed = true;
+ props.diffFile.viewer.name = diffViewerModes.mode_changed;
vm = mountComponentWithStore(Component, { props, store });
@@ -338,7 +336,7 @@ describe('diff_file_header', () => {
});
it('does not display the file mode if it has not changed', () => {
- props.diffFile.mode_changed = false;
+ props.diffFile.viewer.name = diffViewerModes.text;
vm = mountComponentWithStore(Component, { props, store });
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 1af49282c36..65a1c9b8f15 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
+import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import store from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -27,7 +28,6 @@ describe('DiffFile', () => {
expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1);
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
- expect(vm.file.renderIt).toEqual(false);
vm.file.renderIt = true;
vm.$nextTick(() => {
@@ -38,8 +38,8 @@ describe('DiffFile', () => {
describe('collapsed', () => {
it('should not have file content', done => {
expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1);
- expect(vm.file.collapsed).toEqual(false);
- vm.file.collapsed = true;
+ expect(vm.isCollapsed).toEqual(false);
+ vm.isCollapsed = true;
vm.file.renderIt = true;
vm.$nextTick(() => {
@@ -50,9 +50,8 @@ describe('DiffFile', () => {
});
it('should have collapsed text and link', done => {
- vm.file.renderIt = true;
- vm.file.collapsed = false;
- vm.file.highlighted_diff_lines = null;
+ vm.renderIt = true;
+ vm.isCollapsed = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain('This diff is collapsed');
@@ -63,8 +62,8 @@ describe('DiffFile', () => {
});
it('should have collapsed text and link even before rendered', done => {
- vm.file.renderIt = false;
- vm.file.collapsed = true;
+ vm.renderIt = false;
+ vm.isCollapsed = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain('This diff is collapsed');
@@ -75,10 +74,10 @@ describe('DiffFile', () => {
});
it('should be collapsed for renamed files', done => {
- vm.file.renderIt = true;
- vm.file.collapsed = false;
+ vm.renderIt = true;
+ vm.isCollapsed = false;
vm.file.highlighted_diff_lines = null;
- vm.file.renamed_file = true;
+ vm.file.viewer.name = diffViewerModes.renamed;
vm.$nextTick(() => {
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
@@ -88,10 +87,10 @@ describe('DiffFile', () => {
});
it('should be collapsed for mode changed files', done => {
- vm.file.renderIt = true;
- vm.file.collapsed = false;
+ vm.renderIt = true;
+ vm.isCollapsed = false;
vm.file.highlighted_diff_lines = null;
- vm.file.mode_changed = true;
+ vm.file.viewer.name = diffViewerModes.mode_changed;
vm.$nextTick(() => {
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
@@ -101,7 +100,7 @@ describe('DiffFile', () => {
});
it('should have loading icon while loading a collapsed diffs', done => {
- vm.file.collapsed = true;
+ vm.isCollapsed = true;
vm.isLoadingCollapsedDiff = true;
vm.$nextTick(() => {
@@ -116,7 +115,7 @@ describe('DiffFile', () => {
describe('too large diff', () => {
it('should have too large warning and blob link', done => {
const BLOB_LINK = '/file/view/path';
- vm.file.too_large = true;
+ vm.file.viewer.error = diffViewerErrors.too_large;
vm.file.view_path = BLOB_LINK;
vm.$nextTick(() => {
@@ -140,11 +139,11 @@ describe('DiffFile', () => {
vm.file.highlighted_diff_lines = undefined;
vm.file.parallel_diff_lines = [];
- vm.file.collapsed = true;
+ vm.isCollapsed = true;
vm.$nextTick()
.then(() => {
- vm.file.collapsed = false;
+ vm.isCollapsed = false;
return vm.$nextTick();
})
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index 9e556698f34..cd7bf6405e5 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -28,7 +28,7 @@ describe('Diffs tree list component', () => {
localStorage.removeItem('mr_diff_tree_list');
- vm = mountComponentWithStore(Component, { store });
+ vm = mountComponentWithStore(Component, { store, props: { hideFileStats: false } });
});
afterEach(() => {
@@ -77,6 +77,16 @@ describe('Diffs tree list component', () => {
expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app');
});
+ it('hides file stats', done => {
+ vm.hideFileStats = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-row-stats')).toBe(null);
+
+ done();
+ });
+ });
+
it('calls toggleTreeOpen when clicking folder', () => {
spyOn(vm.$store, 'dispatch').and.stub();
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index c1e9f791925..4a091b4580b 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -266,7 +266,7 @@ export default {
blob_name: 'CHANGELOG',
blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>',
file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a',
- file_path: 'CHANGELOG',
+ file_path: 'CHANGELOG.rb',
new_file: false,
deleted_file: false,
renamed_file: false,
@@ -286,7 +286,7 @@ export default {
content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13',
stored_externally: null,
external_storage: null,
- old_path_html: ['CHANGELOG', 'CHANGELOG'],
+ old_path_html: 'CHANGELOG_OLD',
new_path_html: 'CHANGELOG',
context_lines_path:
'/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
@@ -485,6 +485,10 @@ export default {
},
},
],
+ viewer: {
+ name: 'text',
+ error: null,
+ },
},
diff_discussion: true,
truncated_diff_lines: [
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index 031c9842f2f..32af9ea8ddd 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -25,6 +25,8 @@ export default {
text: true,
viewer: {
name: 'text',
+ error: null,
+ collapsed: false,
},
added_lines: 2,
removed_lines: 0,
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index b53ae4cecfd..acff80bca62 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -29,6 +29,7 @@ import actions, {
renderFileForDiscussionId,
setRenderTreeList,
setShowWhitespace,
+ setRenderIt,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -262,12 +263,16 @@ describe('DiffsStoreActions', () => {
{
id: 1,
renderIt: false,
- collapsed: false,
+ viewer: {
+ collapsed: false,
+ },
},
{
id: 2,
renderIt: false,
- collapsed: false,
+ viewer: {
+ collapsed: false,
+ },
},
],
};
@@ -766,7 +771,9 @@ describe('DiffsStoreActions', () => {
diffFiles: [
{
file_hash: 'HASH',
- collapsed,
+ viewer: {
+ collapsed,
+ },
renderIt,
},
],
@@ -849,4 +856,10 @@ describe('DiffsStoreActions', () => {
expect(window.history.pushState).toHaveBeenCalled();
});
});
+
+ describe('setRenderIt', () => {
+ it('commits RENDER_FILE', done => {
+ testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 4f69dc92ab8..0ab88e6b2aa 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -51,13 +51,13 @@ describe('Diffs Module Getters', () => {
describe('hasCollapsedFile', () => {
it('returns true when all files are collapsed', () => {
- localState.diffFiles = [{ collapsed: true }, { collapsed: true }];
+ localState.diffFiles = [{ viewer: { collapsed: true } }, { viewer: { collapsed: true } }];
expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
it('returns true when at least one file is collapsed', () => {
- localState.diffFiles = [{ collapsed: false }, { collapsed: true }];
+ localState.diffFiles = [{ viewer: { collapsed: false } }, { viewer: { collapsed: true } }];
expect(getters.hasCollapsedFile(localState)).toEqual(true);
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index a6f3f9b9dc3..09ee691b602 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -121,8 +121,14 @@ describe('DiffsStoreMutations', () => {
describe('ADD_COLLAPSED_DIFFS', () => {
it('should update the state with the given data for the given file hash', () => {
const fileHash = 123;
- const state = { diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }] };
- const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existing_field: 1 }] };
+ const state = {
+ diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }],
+ };
+ const data = {
+ diff_files: [
+ { file_hash: fileHash, extra_field: 1, existing_field: 1, viewer: { name: 'text' } },
+ ],
+ };
mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data });
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index baf6e111f9f..599ea9cd420 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -601,7 +601,7 @@ describe('DiffsStoreUtils', () => {
it('returns mode_changed if key has no match', () => {
expect(
utils.getDiffMode({
- mode_changed: true,
+ viewer: { name: 'mode_changed' },
}),
).toBe('mode_changed');
});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index 52895f35f3a..ecd28594873 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -31,4 +31,224 @@ describe('Environment table', () => {
expect(vm.$el.getAttribute('class')).toContain('ci-table');
});
+
+ describe('sortEnvironments', () => {
+ it('should sort environments by last updated', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'new',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 1, 5).toISOString(),
+ },
+ },
+ {
+ name: 'older',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2018, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'an environment with no deployment',
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ });
+
+ const [old, newer, older, noDeploy] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([newer, old, older, noDeploy]);
+ });
+
+ it('should push environments with no deployments to the bottom', () => {
+ const mockItems = [
+ {
+ name: 'production',
+ size: 1,
+ id: 2,
+ state: 'available',
+ external_url: 'https://google.com/production',
+ environment_type: null,
+ last_deployment: null,
+ has_stop_action: false,
+ environment_path: '/Commit451/lab-coat/environments/2',
+ stop_path: '/Commit451/lab-coat/environments/2/stop',
+ folder_path: '/Commit451/lab-coat/environments/folders/production',
+ created_at: '2019-01-17T16:26:10.064Z',
+ updated_at: '2019-01-17T16:27:37.717Z',
+ can_stop: true,
+ },
+ {
+ name: 'review/225addcibuildstatus',
+ size: 2,
+ isFolder: true,
+ isLoadingFolderContent: false,
+ folderName: 'review',
+ isOpen: false,
+ children: [],
+ id: 12,
+ state: 'available',
+ external_url: 'https://google.com/review/225addcibuildstatus',
+ environment_type: 'review',
+ last_deployment: null,
+ has_stop_action: false,
+ environment_path: '/Commit451/lab-coat/environments/12',
+ stop_path: '/Commit451/lab-coat/environments/12/stop',
+ folder_path: '/Commit451/lab-coat/environments/folders/review',
+ created_at: '2019-01-17T16:27:37.877Z',
+ updated_at: '2019-01-17T16:27:37.883Z',
+ can_stop: true,
+ },
+ {
+ name: 'staging',
+ size: 1,
+ id: 1,
+ state: 'available',
+ external_url: 'https://google.com/staging',
+ environment_type: null,
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ scheduled_actions: [],
+ },
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ });
+
+ const [prod, review, staging] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([review, staging, prod]);
+ });
+
+ it('should sort environments by folder first', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'new',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 1, 5).toISOString(),
+ },
+ },
+ {
+ name: 'older',
+ size: 3,
+ isFolder: true,
+ children: [],
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ });
+
+ const [old, newer, older] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]);
+ });
+
+ it('should break ties by name', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ isFolder: false,
+ },
+ {
+ name: 'new',
+ isFolder: false,
+ },
+ {
+ folderName: 'older',
+ isFolder: true,
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ });
+
+ const [old, newer, older] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]);
+ });
+ });
+
+ describe('sortedEnvironments', () => {
+ it('it should sort children as well', () => {
+ const mockItems = [
+ {
+ name: 'production',
+ last_deployment: null,
+ },
+ {
+ name: 'review/225addcibuildstatus',
+ isFolder: true,
+ folderName: 'review',
+ isOpen: true,
+ children: [
+ {
+ name: 'review/225addcibuildstatus',
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ },
+ },
+ {
+ name: 'review/master',
+ last_deployment: {
+ created_at: '2019-02-17T16:26:15.125Z',
+ },
+ },
+ ],
+ },
+ {
+ name: 'staging',
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ },
+ },
+ ];
+ const [production, review, staging] = mockItems;
+ const [addcibuildstatus, master] = mockItems[1].children;
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ });
+
+ expect(vm.sortedEnvironments.map(env => env.name)).toEqual([
+ review.name,
+ staging.name,
+ production.name,
+ ]);
+
+ expect(vm.sortedEnvironments[0].children).toEqual([master, addcibuildstatus]);
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index 9aa3cbaa231..6230da77f49 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -755,6 +755,17 @@ describe('Filtered Search Visual Tokens', () => {
expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
});
+ it('does not update user token appearance for `None` filter', () => {
+ const { tokenNameElement } = findElements(authorToken);
+
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'None';
+
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
it('does not update user token appearance for `none` filter', () => {
const { tokenNameElement } = findElements(authorToken);
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
index 1b2a3b484bb..cd66d98f92a 100644
--- a/spec/javascripts/fixtures/blob.rb
+++ b/spec/javascripts/fixtures/blob.rb
@@ -15,6 +15,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
before do
sign_in(admin)
+ allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
after do
diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb
index f0e4bb50c67..295f13b34a4 100644
--- a/spec/javascripts/fixtures/commit.rb
+++ b/spec/javascripts/fixtures/commit.rb
@@ -16,6 +16,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller
before do
project.add_maintainer(user)
sign_in(user)
+ allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
it 'commit/show.html.raw' do |example|
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
index efbda955972..a333d9c0150 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -25,7 +25,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
render_views
it 'deploy_keys/keys.json' do |example|
- create(:deploy_key, public: true)
+ create(:rsa_deploy_key_2048, public: true)
project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
create(:deploy_keys_project, project: project, deploy_key: project_key)
diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb
index f8d55fc97c3..03136f4e661 100644
--- a/spec/javascripts/fixtures/groups.rb
+++ b/spec/javascripts/fixtures/groups.rb
@@ -4,7 +4,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
- let(:group) { create(:group, name: 'frontend-fixtures-group' )}
+ let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre')}
render_views
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 18fb1bebf8b..9b8e90c2a43 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin) }
+ let(:admin) { create(:admin, feed_token: 'feedtoken:coldfeed') }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project_empty_repo, namespace: namespace, path: 'issues-project') }
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 26e81f06c0b..eb37be87e1d 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -35,6 +35,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
before do
sign_in(admin)
+ allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo'])
end
after do
@@ -54,8 +55,10 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
end
it 'merge_requests/merged_merge_request.html.raw' do |example|
- allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true)
- allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true)
+ expect_next_instance_of(MergeRequest) do |merge_request|
+ allow(merge_request).to receive(:source_branch_exists?).and_return(true)
+ allow(merge_request).to receive(:can_remove_source_branch?).and_return(true)
+ end
render_merge_request(example.description, merged_merge_request)
end
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 9b48646f8f0..85f02923804 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe 'Projects (JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
+ runners_token = 'runnerstoken:intabulasreferre'
+
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project', runners_token: runners_token) }
let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') }
- let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') }
- let!(:variable1) { create(:ci_variable, project: project_variable_populated) }
- let!(:variable2) { create(:ci_variable, project: project_variable_populated) }
+ let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2', runners_token: runners_token) }
render_views
@@ -20,6 +20,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
before do
project.add_maintainer(admin)
sign_in(admin)
+ allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
after do
@@ -70,6 +71,9 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
end
it 'projects/ci_cd_settings_with_variables.html.raw' do |example|
+ create(:ci_variable, project: project_variable_populated)
+ create(:ci_variable, project: project_variable_populated)
+
get :show, params: {
namespace_id: project_variable_populated.namespace.to_param,
project_id: project_variable_populated
diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb
index a14837e4d4a..bcd6546f3df 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -7,7 +7,6 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) }
- let!(:snippet_note) { create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') }
render_views
@@ -17,6 +16,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
before do
sign_in(admin)
+ allow(Discussion).to receive(:build_discussion_id).and_return(['discussionid:ceterumcenseo'])
end
after do
@@ -24,6 +24,8 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
end
it 'snippets/show.html.raw' do |example|
+ create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item')
+
get(:show, params: { id: snippet.to_param })
expect(response).to be_success
diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb
index f0aa874bf75..5cdbadef639 100644
--- a/spec/javascripts/fixtures/u2f.rb
+++ b/spec/javascripts/fixtures/u2f.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
context 'U2F' do
include JavaScriptFixturesHelpers
- let(:user) { create(:user, :two_factor_via_u2f) }
+ let(:user) { create(:user, :two_factor_via_u2f, otp_secret: 'otpsecret:coolkids') }
before(:all) do
clean_frontend_fixtures('u2f/')
@@ -33,6 +33,7 @@ context 'U2F' do
before do
sign_in(user)
+ allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
end
it 'u2f/register.html.raw' do |example|
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index 1972408356e..88652202a8e 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -84,7 +84,10 @@ export default (
done();
};
- const result = action({ commit, state, dispatch, rootState: state, rootGetters: state }, payload);
+ const result = action(
+ { commit, state, dispatch, rootState: state, rootGetters: state, getters: state },
+ payload,
+ );
return new Promise(resolve => {
setImmediate(resolve);
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index 595a2f927e9..d94cc1a8faa 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -41,6 +41,15 @@ describe('new file modal component', () => {
expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name');
});
+ it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => {
+ const templateFilesEl = vm.$el.querySelector('.file-templates');
+ if (type === 'tree') {
+ expect(templateFilesEl).toBeNull();
+ } else {
+ expect(templateFilesEl instanceof Element).toBeTruthy();
+ }
+ });
+
describe('createEntryInStore', () => {
it('$emits create', () => {
spyOn(vm, 'createTempEntry');
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..a1ff84ce259
--- /dev/null
+++ b/spec/javascripts/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,186 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import store from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ let mock;
+ const reposPath = '/repos-path';
+ const jobsPath = '/jobs-path';
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function createComponent() {
+ const ImportProjectsTable = Vue.extend(importProjectsTable);
+
+ const component = new ImportProjectsTable({
+ store,
+ propsData: {
+ providerTitle,
+ },
+ }).$mount();
+
+ component.$store.dispatch('stopJobsPolling');
+
+ return component;
+ }
+
+ beforeEach(() => {
+ store.dispatch('setInitialData', { reposPath });
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('renders a loading icon whilst repos are loading', done => {
+ mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
+
+ vm = createComponent();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ })
+ .then(() => done())
+ .catch(() => done.fail());
+ });
+
+ it('renders a table with imported projects and provider repos', done => {
+ const response = {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ };
+ mock.onGet(reposPath).reply(200, response);
+
+ vm = createComponent();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ })
+ .then(() => done())
+ .catch(() => done.fail());
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', done => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ };
+ mock.onGet(reposPath).reply(200, response);
+
+ vm = createComponent();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ })
+ .then(() => done())
+ .catch(() => done.fail());
+ });
+
+ it('imports provider repos if bulk import button is clicked', done => {
+ const importPath = '/import-path';
+ const response = {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ };
+
+ mock.onGet(reposPath).replyOnce(200, response);
+ mock.onPost(importPath).replyOnce(200, importedProject);
+
+ store.dispatch('setInitialData', { importPath });
+
+ vm = createComponent();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => setTimeoutPromise())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ })
+ .then(() => done())
+ .catch(() => done.fail());
+ });
+
+ it('polls to update the status of imported projects', done => {
+ const importPath = '/import-path';
+ const response = {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ };
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ mock.onGet(reposPath).replyOnce(200, response);
+
+ store.dispatch('setInitialData', { importPath, jobsPath });
+
+ vm = createComponent();
+
+ setTimeoutPromise()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ mock.onGet(jobsPath).replyOnce(200, updatedProjects);
+ return vm.$store.dispatch('restartJobsPolling');
+ })
+ .then(() => setTimeoutPromise())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ })
+ .then(() => done())
+ .catch(() => done.fail());
+ });
+});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
new file mode 100644
index 00000000000..8af3b5954a9
--- /dev/null
+++ b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
@@ -0,0 +1,50 @@
+import Vue from 'vue';
+import store from '~/import_projects/store';
+import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportedProjectTableRow', () => {
+ let vm;
+ const project = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'finished',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function createComponent() {
+ const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
+
+ return new ImportedProjectTableRow({
+ store,
+ propsData: {
+ project: {
+ ...project,
+ },
+ },
+ }).$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders an imported project table row', () => {
+ vm = createComponent();
+
+ const providerLink = vm.$el.querySelector('.js-provider-link');
+ const statusObject = STATUS_MAP[project.importStatus];
+
+ expect(vm.$el.classList.contains('js-imported-project')).toBe(true);
+ expect(providerLink.href).toMatch(project.providerLink);
+ expect(providerLink.textContent).toMatch(project.importSource);
+ expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath);
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath);
+ });
+});
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
new file mode 100644
index 00000000000..69377f8d685
--- /dev/null
+++ b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import store from '~/import_projects/store';
+import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
+import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
+
+describe('ProviderRepoTableRow', () => {
+ let vm;
+ const repo = {
+ id: 10,
+ sanitizedName: 'sanitizedName',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ };
+
+ function createComponent() {
+ const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
+
+ return new ProviderRepoTableRow({
+ store,
+ propsData: {
+ repo: {
+ ...repo,
+ },
+ },
+ }).$mount();
+ }
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a provider repo table row', () => {
+ vm = createComponent();
+
+ const providerLink = vm.$el.querySelector('.js-provider-link');
+ const statusObject = STATUS_MAP[STATUSES.NONE];
+
+ expect(vm.$el.classList.contains('js-provider-repo')).toBe(true);
+ expect(providerLink.href).toMatch(repo.providerLink);
+ expect(providerLink.textContent).toMatch(repo.fullName);
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ expect(vm.$el.querySelector('.js-import-button')).not.toBeNull();
+ });
+
+ it('renders a select2 namespace select', () => {
+ vm = createComponent();
+
+ const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
+
+ expect(dropdownTrigger).not.toBeNull();
+ expect(dropdownTrigger.classList.contains('select2-container')).toBe(true);
+
+ dropdownTrigger.click();
+
+ expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
+ });
+
+ it('imports repo when clicking import button', done => {
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
+ const mock = new MockAdapter(axios);
+
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+ mock.onPost(importPath).replyOnce(200);
+ spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
+
+ vm = createComponent();
+
+ vm.$el.querySelector('.js-import-button').click();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ })
+ .then(() => mock.restore())
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/javascripts/import_projects/store/actions_spec.js
new file mode 100644
index 00000000000..77850ee3283
--- /dev/null
+++ b/spec/javascripts/import_projects/store/actions_spec.js
@@ -0,0 +1,284 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ SET_INITIAL_DATA,
+ REQUEST_REPOS,
+ RECEIVE_REPOS_SUCCESS,
+ RECEIVE_REPOS_ERROR,
+ REQUEST_IMPORT,
+ RECEIVE_IMPORT_SUCCESS,
+ RECEIVE_IMPORT_ERROR,
+ RECEIVE_JOBS_SUCCESS,
+} from '~/import_projects/store/mutation_types';
+import {
+ setInitialData,
+ requestRepos,
+ receiveReposSuccess,
+ receiveReposError,
+ fetchRepos,
+ requestImport,
+ receiveImportSuccess,
+ receiveImportError,
+ fetchImport,
+ receiveJobsSuccess,
+ fetchJobs,
+ clearJobsEtagPoll,
+ stopJobsPolling,
+} from '~/import_projects/store/actions';
+import state from '~/import_projects/store/state';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+
+describe('import_projects store actions', () => {
+ let localState;
+ const repoId = 1;
+ const repos = [{ id: 1 }, { id: 2 }];
+ const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } };
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('setInitialData', () => {
+ it(`commits ${SET_INITIAL_DATA} mutation`, done => {
+ const initialData = {
+ reposPath: 'reposPath',
+ provider: 'provider',
+ jobsPath: 'jobsPath',
+ importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath',
+ defaultTargetNamespace: 'defaultTargetNamespace',
+ ciCdOnly: 'ciCdOnly',
+ canSelectNamespace: 'canSelectNamespace',
+ };
+
+ testAction(
+ setInitialData,
+ initialData,
+ localState,
+ [{ type: SET_INITIAL_DATA, payload: initialData }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestRepos', () => {
+ it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => {
+ testAction(
+ requestRepos,
+ null,
+ localState,
+ [{ type: REQUEST_REPOS, payload: null }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReposSuccess', () => {
+ it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => {
+ testAction(
+ receiveReposSuccess,
+ repos,
+ localState,
+ [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveReposError', () => {
+ it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => {
+ testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done);
+ });
+ });
+
+ describe('fetchRepos', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.reposPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => {
+ const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [
+ { type: 'requestRepos' },
+ {
+ type: 'receiveReposSuccess',
+ payload: convertObjectPropsToCamelCase(payload, { deep: true }),
+ },
+ {
+ type: 'fetchJobs',
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => {
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+
+ testAction(
+ fetchRepos,
+ null,
+ localState,
+ [],
+ [{ type: 'requestRepos' }, { type: 'receiveReposError' }],
+ done,
+ );
+ });
+ });
+
+ describe('requestImport', () => {
+ it(`commits ${REQUEST_IMPORT} mutation`, done => {
+ testAction(
+ requestImport,
+ repoId,
+ localState,
+ [{ type: REQUEST_IMPORT, payload: repoId }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveImportSuccess', () => {
+ it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => {
+ const payload = { importedProject: { name: 'imported/project' }, repoId: 2 };
+
+ testAction(
+ receiveImportSuccess,
+ payload,
+ localState,
+ [{ type: RECEIVE_IMPORT_SUCCESS, payload }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveImportError', () => {
+ it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => {
+ testAction(
+ receiveImportError,
+ repoId,
+ localState,
+ [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchImport', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.importPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => {
+ const importedProject = { name: 'imported/project' };
+ const importRepoId = importPayload.repo.id;
+ mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
+
+ testAction(
+ fetchImport,
+ importPayload,
+ localState,
+ [],
+ [
+ { type: 'requestImport', payload: importRepoId },
+ {
+ type: 'receiveImportSuccess',
+ payload: {
+ importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }),
+ repoId: importRepoId,
+ },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => {
+ mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
+
+ testAction(
+ fetchImport,
+ importPayload,
+ localState,
+ [],
+ [
+ { type: 'requestImport', payload: importPayload.repo.id },
+ { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('receiveJobsSuccess', () => {
+ it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => {
+ testAction(
+ receiveJobsSuccess,
+ repos,
+ localState,
+ [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchJobs', () => {
+ let mock;
+
+ beforeEach(() => {
+ localState.jobsPath = `${TEST_HOST}/endpoint.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ stopJobsPolling();
+ clearJobsEtagPoll();
+ });
+
+ afterEach(() => mock.restore());
+
+ it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => {
+ const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
+
+ testAction(
+ fetchJobs,
+ null,
+ localState,
+ [],
+ [
+ {
+ type: 'receiveJobsSuccess',
+ payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }),
+ },
+ ],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/javascripts/import_projects/store/getters_spec.js
new file mode 100644
index 00000000000..e5e4a95f473
--- /dev/null
+++ b/spec/javascripts/import_projects/store/getters_spec.js
@@ -0,0 +1,83 @@
+import {
+ namespaceSelectOptions,
+ isImportingAnyRepo,
+ hasProviderRepos,
+ hasImportedProjects,
+} from '~/import_projects/store/getters';
+import state from '~/import_projects/store/state';
+
+describe('import_projects store getters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('namespaceSelectOptions', () => {
+ const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }];
+ const defaultTargetNamespace = 'current-user';
+
+ it('returns an options array with a "Users" and "Groups" optgroups', () => {
+ localState.namespaces = namespaces;
+ localState.defaultTargetNamespace = defaultTargetNamespace;
+
+ const optionsArray = namespaceSelectOptions(localState);
+ const groupsGroup = optionsArray[0];
+ const usersGroup = optionsArray[1];
+
+ expect(groupsGroup.text).toBe('Groups');
+ expect(usersGroup.text).toBe('Users');
+
+ groupsGroup.children.forEach((child, index) => {
+ expect(child.id).toBe(namespaces[index].fullPath);
+ expect(child.text).toBe(namespaces[index].fullPath);
+ });
+
+ expect(usersGroup.children.length).toBe(1);
+ expect(usersGroup.children[0].id).toBe(defaultTargetNamespace);
+ expect(usersGroup.children[0].text).toBe(defaultTargetNamespace);
+ });
+ });
+
+ describe('isImportingAnyRepo', () => {
+ it('returns true if there are any reposBeingImported', () => {
+ localState.reposBeingImported = new Array(1);
+
+ expect(isImportingAnyRepo(localState)).toBe(true);
+ });
+
+ it('returns false if there are no reposBeingImported', () => {
+ localState.reposBeingImported = [];
+
+ expect(isImportingAnyRepo(localState)).toBe(false);
+ });
+ });
+
+ describe('hasProviderRepos', () => {
+ it('returns true if there are any providerRepos', () => {
+ localState.providerRepos = new Array(1);
+
+ expect(hasProviderRepos(localState)).toBe(true);
+ });
+
+ it('returns false if there are no providerRepos', () => {
+ localState.providerRepos = [];
+
+ expect(hasProviderRepos(localState)).toBe(false);
+ });
+ });
+
+ describe('hasImportedProjects', () => {
+ it('returns true if there are any importedProjects', () => {
+ localState.importedProjects = new Array(1);
+
+ expect(hasImportedProjects(localState)).toBe(true);
+ });
+
+ it('returns false if there are no importedProjects', () => {
+ localState.importedProjects = [];
+
+ expect(hasImportedProjects(localState)).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/javascripts/import_projects/store/mutations_spec.js
new file mode 100644
index 00000000000..8db8e9819ba
--- /dev/null
+++ b/spec/javascripts/import_projects/store/mutations_spec.js
@@ -0,0 +1,34 @@
+import * as types from '~/import_projects/store/mutation_types';
+import mutations from '~/import_projects/store/mutations';
+
+describe('import_projects store mutations', () => {
+ describe(types.RECEIVE_IMPORT_SUCCESS, () => {
+ it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => {
+ const repoId = 1;
+ const state = {
+ reposBeingImported: [repoId],
+ providerRepos: [{ id: repoId }],
+ importedProjects: [],
+ };
+ const importedProject = { id: repoId };
+
+ mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId });
+
+ expect(state.reposBeingImported.includes(repoId)).toBe(false);
+ expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false);
+ expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true);
+ });
+ });
+
+ describe(types.RECEIVE_JOBS_SUCCESS, () => {
+ it('updates importStatus of existing importedProjects', () => {
+ const repoId = 1;
+ const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] };
+ const updatedProjects = [{ id: repoId, importStatus: 'finished' }];
+
+ mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
+
+ expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 3eff3f655ee..0bb43c94f6a 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -409,13 +409,6 @@ describe('common_utils', () => {
});
});
- describe('convertPermissionToBoolean', () => {
- it('should convert a boolean in a string to a boolean', () => {
- expect(commonUtils.convertPermissionToBoolean('true')).toEqual(true);
- expect(commonUtils.convertPermissionToBoolean('false')).toEqual(false);
- });
- });
-
describe('backOff', () => {
beforeEach(() => {
// shortcut our timeouts otherwise these tests will take a long time to finish
@@ -855,6 +848,7 @@ describe('common_utils', () => {
});
it('returns true when provided `el` is in viewport', () => {
+ el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`);
document.body.appendChild(el);
expect(commonUtils.isInViewport(el)).toBe(true);
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index d0da659c3d7..138041a349f 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -153,6 +153,36 @@ describe('Poll', () => {
});
});
+ describe('enable', () => {
+ it('should enable polling upon a response', done => {
+ jasmine.clock().install();
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {},
+ });
+
+ Polling.enable({
+ data: { page: 4 },
+ response: { status: 200, headers: { 'poll-interval': 1 } },
+ });
+
+ jasmine.clock().tick(1);
+ jasmine.clock().uninstall();
+
+ waitForAllCallsToFinish(service, 1, () => {
+ Polling.stop();
+
+ expect(service.fetch.calls.count()).toEqual(1);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
+ expect(Polling.options.data).toEqual({ page: 4 });
+ done();
+ });
+ });
+ });
+
describe('restart', () => {
it('should restart polling when its called', done => {
mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
@@ -171,6 +201,7 @@ describe('Poll', () => {
});
spyOn(Polling, 'stop').and.callThrough();
+ spyOn(Polling, 'enable').and.callThrough();
spyOn(Polling, 'restart').and.callThrough();
Polling.makeRequest();
@@ -181,6 +212,7 @@ describe('Poll', () => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
expect(Polling.stop).toHaveBeenCalled();
+ expect(Polling.enable).toHaveBeenCalled();
expect(Polling.restart).toHaveBeenCalled();
expect(Polling.options.data).toEqual({ page: 4 });
done();
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
index 0b36fc9f5f7..d334ef7ba4f 100644
--- a/spec/javascripts/monitoring/charts/area_spec.js
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -127,7 +127,7 @@ describe('Area component', () => {
});
it('formats tooltip content', () => {
- expect(areaChart.vm.tooltip.content).toBe('CPU (Cores) 5.556');
+ expect(areaChart.vm.tooltip.content).toBe('CPU 5.556');
});
});
@@ -213,7 +213,7 @@ describe('Area component', () => {
describe('yAxisLabel', () => {
it('constructs a label for the chart y-axis', () => {
- expect(areaChart.vm.yAxisLabel).toBe('CPU (Cores)');
+ expect(areaChart.vm.yAxisLabel).toBe('CPU');
});
});
});
diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js
new file mode 100644
index 00000000000..52d2e7ce947
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_filter_note_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue';
+import eventHub from '~/notes/event_hub';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('DiscussionFilterNote component', () => {
+ let vm;
+
+ const createComponent = () => {
+ const Component = Vue.extend(DiscussionFilterNote);
+
+ return mountComponent(Component);
+ };
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('timelineContent', () => {
+ it('returns string containing instruction for switching feed type', () => {
+ expect(vm.timelineContent).toBe(
+ "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('selectFilter', () => {
+ it('emits `dropdownSelect` event on `eventHub` with provided param', () => {
+ spyOn(eventHub, '$emit');
+
+ vm.selectFilter(1);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element', () => {
+ expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true);
+ });
+
+ it('renders comment icon element', () => {
+ expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain(
+ 'comment',
+ );
+ });
+
+ it('renders filter information note', () => {
+ expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain(
+ "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+
+ it('renders filter buttons', () => {
+ const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions');
+
+ expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain(
+ 'Show all activity',
+ );
+
+ expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain(
+ 'Show comments only',
+ );
+ });
+
+ it('clicking `Show all activity` button calls `selectFilter("all")` method', () => {
+ const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child');
+ spyOn(vm, 'selectFilter');
+
+ showAllBtn.dispatchEvent(new Event('click'));
+
+ expect(vm.selectFilter).toHaveBeenCalledWith(0);
+ });
+
+ it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => {
+ const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child');
+ spyOn(vm, 'selectFilter');
+
+ showAllBtn.dispatchEvent(new Event('click'));
+
+ expect(vm.selectFilter).toHaveBeenCalledWith(1);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
index 91dab58ba7f..1c366aee8e2 100644
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import createStore from '~/notes/stores';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { discussionFiltersMock, discussionMock } from '../mock_data';
@@ -54,14 +54,18 @@ describe('DiscussionFilter component', () => {
});
it('updates to the selected item', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
filterItem.click();
expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim());
});
it('only updates when selected filter changes', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
+ );
spyOn(vm, 'filterDiscussion');
filterItem.click();
@@ -70,21 +74,27 @@ describe('DiscussionFilter component', () => {
});
it('disables commenting when "Show history only" filter is applied', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
filterItem.click();
expect(vm.$store.state.commentsDisabled).toBe(true);
});
it('enables commenting when "Show history only" filter is not applied', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
+ );
filterItem.click();
expect(vm.$store.state.commentsDisabled).toBe(false);
});
it('renders a dropdown divider for the default filter', () => {
- const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child');
+ const defaultFilter = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
+ );
expect(defaultFilter.lastChild.classList).toContain('dropdown-divider');
});
diff --git a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js
deleted file mode 100644
index c41b29fa788..00000000000
--- a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import jumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-
-const localVue = createLocalVue();
-
-describe('jumpToNextDiscussionButton', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = shallowMount(jumpToNextDiscussionButton, {
- localVue,
- sync: false,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('emits onClick event on button click', done => {
- const button = wrapper.find({ ref: 'button' });
-
- button.trigger('click');
-
- localVue.nextTick(() => {
- expect(wrapper.emitted()).toEqual({
- onClick: [[]],
- });
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js
new file mode 100644
index 00000000000..b2a91c9919a
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -0,0 +1,31 @@
+import { GlButton } from '@gitlab/ui';
+import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { TEST_HOST } from 'spec/test_constants';
+
+const localVue = createLocalVue();
+
+describe('ResolveWithIssueButton', () => {
+ let wrapper;
+ const url = `${TEST_HOST}/hello-world/`;
+
+ beforeEach(() => {
+ wrapper = shallowMount(ResolveWithIssueButton, {
+ localVue,
+ sync: false,
+ propsData: {
+ url,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('it should have a link with the provided link property as href', () => {
+ const button = wrapper.find(GlButton);
+
+ expect(button.attributes().href).toBe(url);
+ });
+});
diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js
index 11e1664a3f4..11fb89808d9 100644
--- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js
+++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js
@@ -3,27 +3,14 @@ import { createLocalVue, mount } from '@vue/test-utils';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
describe('ReplyButton', () => {
- const noteId = 'dummy-note-id';
-
let wrapper;
- let convertToDiscussion;
beforeEach(() => {
const localVue = createLocalVue();
- convertToDiscussion = jasmine.createSpy('convertToDiscussion');
localVue.use(Vuex);
- const store = new Vuex.Store({
- actions: {
- convertToDiscussion,
- },
- });
wrapper = mount(ReplyButton, {
- propsData: {
- noteId,
- },
- store,
sync: false,
localVue,
});
@@ -33,14 +20,13 @@ describe('ReplyButton', () => {
wrapper.destroy();
});
- it('dispatches convertToDiscussion with note ID on click', () => {
+ it('emits startReplying on click', () => {
const button = wrapper.find({ ref: 'button' });
button.trigger('click');
- expect(convertToDiscussion).toHaveBeenCalledTimes(1);
- const [, payload] = convertToDiscussion.calls.argsFor(0);
-
- expect(payload).toBe(noteId);
+ expect(wrapper.emitted()).toEqual({
+ startReplying: [[]],
+ });
});
});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index d5c0bf6b25d..d716ece3766 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -83,6 +83,8 @@ describe('note_app', () => {
describe('render', () => {
beforeEach(() => {
+ setFixtures('<div class="js-discussions-count"></div>');
+
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
});
@@ -124,9 +126,24 @@ describe('note_app', () => {
expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
});
+ it('should render discussion filter note `commentsDisabled` is true', () => {
+ store.state.commentsDisabled = true;
+ wrapper = mountComponent();
+
+ expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ });
+
it('should render form comment button as disabled', () => {
expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled');
});
+
+ it('updates discussions badge', done => {
+ setTimeout(() => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
+
+ done();
+ });
+ });
});
describe('while fetching data', () => {
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 2eae22e095f..2b93fb9fb45 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -2,6 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file';
@@ -238,4 +239,42 @@ describe('noteable_discussion component', () => {
});
});
});
+
+ describe('for resolved discussion', () => {
+ beforeEach(() => {
+ const discussion = getJSONFixture(discussionWithTwoUnresolvedNotes)[0];
+ wrapper.setProps({ discussion });
+ });
+
+ it('does not display a button to resolve with issue', () => {
+ const button = wrapper.find(ResolveWithIssueButton);
+
+ expect(button.exists()).toBe(false);
+ });
+ });
+
+ describe('for unresolved discussion', () => {
+ beforeEach(done => {
+ const discussion = {
+ ...getJSONFixture(discussionWithTwoUnresolvedNotes)[0],
+ expanded: true,
+ };
+ discussion.notes = discussion.notes.map(note => ({
+ ...note,
+ resolved: false,
+ }));
+
+ wrapper.setProps({ discussion });
+ wrapper.vm
+ .$nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays a button to resolve with issue', () => {
+ const button = wrapper.find(ResolveWithIssueButton);
+
+ expect(button.exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 73f960dd21e..94ce6d8e222 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
import $ from 'jquery';
import _ from 'underscore';
+import { TEST_HOST } from 'spec/test_constants';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import * as actions from '~/notes/stores/actions';
+import * as mutationTypes from '~/notes/stores/mutation_types';
+import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
import testAction from '../../helpers/vuex_action_helper';
@@ -599,4 +602,153 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('updateOrCreateNotes', () => {
+ let commit;
+ let dispatch;
+ let state;
+
+ beforeEach(() => {
+ commit = jasmine.createSpy('commit');
+ dispatch = jasmine.createSpy('dispatch');
+ state = {};
+ });
+
+ afterEach(() => {
+ commit.calls.reset();
+ dispatch.calls.reset();
+ });
+
+ it('Updates existing note', () => {
+ const note = { id: 1234 };
+ const getters = { notesById: { 1234: note } };
+
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
+
+ expect(commit.calls.allArgs()).toEqual([[mutationTypes.UPDATE_NOTE, note]]);
+ });
+
+ it('Creates a new note if none exisits', () => {
+ const note = { id: 1234 };
+ const getters = { notesById: {} };
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]);
+
+ expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]);
+ });
+
+ describe('Discussion notes', () => {
+ let note;
+ let getters;
+
+ beforeEach(() => {
+ note = { id: 1234 };
+ getters = { notesById: {} };
+ });
+
+ it('Adds a reply to an existing discussion', () => {
+ state = { discussions: [note] };
+ const discussionNote = {
+ ...note,
+ type: notesConstants.DISCUSSION_NOTE,
+ discussion_id: 1234,
+ };
+
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]);
+
+ expect(commit.calls.allArgs()).toEqual([
+ [mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, discussionNote],
+ ]);
+ });
+
+ it('fetches discussions for diff notes', () => {
+ state = { discussions: [], notesData: { discussionsPath: 'Hello world' } };
+ const diffNote = { ...note, type: notesConstants.DIFF_NOTE, discussion_id: 1234 };
+
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [diffNote]);
+
+ expect(dispatch.calls.allArgs()).toEqual([
+ ['fetchDiscussions', { path: state.notesData.discussionsPath }],
+ ]);
+ });
+
+ it('Adds a new note', () => {
+ state = { discussions: [] };
+ const discussionNote = {
+ ...note,
+ type: notesConstants.DISCUSSION_NOTE,
+ discussion_id: 1234,
+ };
+
+ actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]);
+
+ expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]);
+ });
+ });
+ });
+
+ describe('replyToDiscussion', () => {
+ let res = { discussion: { notes: [] } };
+ const payload = { endpoint: TEST_HOST, data: {} };
+ const interceptor = (request, next) => {
+ next(
+ request.respondWith(JSON.stringify(res), {
+ status: 200,
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ });
+
+ it('updates discussion if response contains disussion', done => {
+ testAction(
+ actions.replyToDiscussion,
+ payload,
+ {
+ notesById: {},
+ },
+ [{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }],
+ [
+ { type: 'updateMergeRequestWidget' },
+ { type: 'startTaskList' },
+ { type: 'updateResolvableDiscussonsCounts' },
+ ],
+ done,
+ );
+ });
+
+ it('adds a reply to a discussion', done => {
+ res = {};
+
+ testAction(
+ actions.replyToDiscussion,
+ payload,
+ {
+ notesById: {},
+ },
+ [{ type: mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, payload: res }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('removeConvertedDiscussion', () => {
+ it('commits CONVERT_TO_DISCUSSION with noteId', done => {
+ const noteId = 'dummy-id';
+ testAction(
+ actions.removeConvertedDiscussion,
+ noteId,
+ {},
+ [{ type: 'REMOVE_CONVERTED_DISCUSSION', payload: noteId }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 4f8d3069bb5..fcad1f245b6 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -527,17 +527,32 @@ describe('Notes Store mutations', () => {
id: 42,
individual_note: true,
};
- state = { discussions: [discussion] };
+ state = { convertedDisscussionIds: [] };
});
- it('toggles individual_note', () => {
+ it('adds a disucssion to convertedDisscussionIds', () => {
mutations.CONVERT_TO_DISCUSSION(state, discussion.id);
- expect(discussion.individual_note).toBe(false);
+ expect(state.convertedDisscussionIds).toContain(discussion.id);
});
+ });
+
+ describe('REMOVE_CONVERTED_DISCUSSION', () => {
+ let discussion;
+ let state;
+
+ beforeEach(() => {
+ discussion = {
+ id: 42,
+ individual_note: true,
+ };
+ state = { convertedDisscussionIds: [41, 42] };
+ });
+
+ it('removes a disucssion from convertedDisscussionIds', () => {
+ mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id);
- it('throws if discussion was not found', () => {
- expect(() => mutations.CONVERT_TO_DISCUSSION(state, 99)).toThrow();
+ expect(state.convertedDisscussionIds).not.toContain(discussion.id);
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index e355416bd27..42bf3b7df09 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -6,32 +6,36 @@ import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Deployment component', () => {
const Component = Vue.extend(deploymentComponent);
- const deploymentMockData = {
- id: 15,
- name: 'review/diplo',
- url: '/root/review-apps/environments/15',
- stop_url: '/root/review-apps/environments/15/stop',
- metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
- metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
- external_url: 'http://gitlab.com.',
- external_url_formatted: 'gitlab',
- deployed_at: '2017-03-22T22:44:42.258Z',
- deployed_at_formatted: 'Mar 22, 2017 10:44pm',
- changes: [
- {
- path: 'index.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
- },
- {
- path: 'imgs/gallery.html',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
- },
- {
- path: 'about/',
- external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
- },
- ],
- };
+ let deploymentMockData;
+
+ beforeEach(() => {
+ deploymentMockData = {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/review-apps/environments/15',
+ stop_url: '/root/review-apps/environments/15/stop',
+ metrics_url: '/root/review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/review-apps/environments/15/metrics',
+ external_url: 'http://gitlab.com.',
+ external_url_formatted: 'gitlab',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ changes: [
+ {
+ path: 'index.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/index.html',
+ },
+ {
+ path: 'imgs/gallery.html',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/imgs/gallery.html',
+ },
+ {
+ path: 'about/',
+ external_url: 'http://root-master-patch-91341.volatile-watch.surge.sh/about/',
+ },
+ ],
+ };
+ });
let vm;
@@ -207,6 +211,31 @@ describe('Deployment component', () => {
});
});
+ describe('with a single change', () => {
+ beforeEach(() => {
+ deploymentMockData.changes = deploymentMockData.changes.slice(0, 1);
+
+ vm = mountComponent(Component, {
+ deployment: { ...deploymentMockData },
+ showMetrics: true,
+ });
+ });
+
+ it('renders the link to the review app without dropdown', () => {
+ expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
+ expect(vm.$el.querySelector('.js-deploy-url-feature-flag')).not.toBeNull();
+ });
+
+ it('renders the link to the review app linked to to the first change', () => {
+ const expectedUrl = deploymentMockData.changes[0].external_url;
+ const deployUrl = vm.$el.querySelector('.js-deploy-url-feature-flag');
+
+ expect(vm.$el.querySelector('.js-mr-wigdet-deployment-dropdown')).toBeNull();
+ expect(deployUrl).not.toBeNull();
+ expect(deployUrl.href).toEqual(expectedUrl);
+ });
+ });
+
describe('deployment status', () => {
describe('running', () => {
beforeEach(() => {
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index ff08a46b922..3e8f73646c8 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -21,6 +21,7 @@ describe('mrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
beforeEach(() => {
+ gon.features = { approvalRules: false };
// Prevent component mounting
delete mrWidgetOptions.el;
@@ -31,6 +32,7 @@ describe('mrWidgetOptions', () => {
});
afterEach(() => {
+ gon.features = null;
vm.$destroy();
});
diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js
index 5b1038840c7..634ba8403d5 100644
--- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js
@@ -5,27 +5,40 @@ import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('Changed file icon', () => {
let vm;
- beforeEach(() => {
+ function factory(props = {}) {
const component = Vue.extend(changedFileIcon);
vm = createComponent(component, {
+ ...props,
file: {
tempFile: false,
changed: true,
},
});
- });
+ }
afterEach(() => {
vm.$destroy();
});
+ it('centers icon', () => {
+ factory({
+ isCentered: true,
+ });
+
+ expect(vm.$el.classList).toContain('ml-auto');
+ });
+
describe('changedIcon', () => {
it('equals file-modified when not a temp file and has changes', () => {
+ factory();
+
expect(vm.changedIcon).toBe('file-modified');
});
it('equals file-addition when a temp file', () => {
+ factory();
+
vm.file.tempFile = true;
expect(vm.changedIcon).toBe('file-addition');
@@ -34,10 +47,14 @@ describe('Changed file icon', () => {
describe('changedIconClass', () => {
it('includes file-modified when not a temp file', () => {
+ factory();
+
expect(vm.changedIconClass).toContain('file-modified');
});
it('includes file-addition when a temp file', () => {
+ factory();
+
vm.file.tempFile = true;
expect(vm.changedIconClass).toContain('file-addition');
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
index 6add6cdac4d..660eaddf01f 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js
@@ -22,6 +22,7 @@ describe('DiffViewer', () => {
createComponent({
diffMode: 'replaced',
+ diffViewerMode: 'image',
newPath: GREEN_BOX_IMAGE_URL,
newSha: 'ABC',
oldPath: RED_BOX_IMAGE_URL,
@@ -45,6 +46,7 @@ describe('DiffViewer', () => {
it('renders fallback download diff display', done => {
createComponent({
diffMode: 'replaced',
+ diffViewerMode: 'added',
newPath: 'test.abc',
newSha: 'ABC',
oldPath: 'testold.abc',
@@ -72,6 +74,7 @@ describe('DiffViewer', () => {
it('renders renamed component', () => {
createComponent({
diffMode: 'renamed',
+ diffViewerMode: 'renamed',
newPath: 'test.abc',
newSha: 'ABC',
oldPath: 'testold.abc',
@@ -84,6 +87,7 @@ describe('DiffViewer', () => {
it('renders mode changed component', () => {
createComponent({
diffMode: 'mode_changed',
+ diffViewerMode: 'image',
newPath: 'test.abc',
newSha: 'ABC',
oldPath: 'testold.abc',
diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
index 49a580be06b..caabe3aa260 100644
--- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js
+++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
@@ -44,7 +44,10 @@ describe('Panel Resizer component', () => {
});
expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-left');
+ expect(vm.$el.getAttribute('class')).toBe(
+ 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0',
+ );
+
expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;');
});
@@ -55,7 +58,9 @@ describe('Panel Resizer component', () => {
});
expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-right');
+ expect(vm.$el.getAttribute('class')).toBe(
+ 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0',
+ );
});
it('drag the resizer', () => {
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index 2890aa4ae38..6e215ea1561 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe API::Helpers::Pagination do
let(:resource) { Project.all }
+ let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:8080/api/v4/projects" }
+ let(:canonical_api_projects_url) { "#{Gitlab.config.gitlab.url}/api/v4/projects" }
subject do
Class.new.include(described_class).new
@@ -9,13 +11,19 @@ describe API::Helpers::Pagination do
describe '#paginate (keyset pagination)' do
let(:value) { spy('return value') }
+ let(:base_query) do
+ {
+ pagination: 'keyset',
+ foo: 'bar',
+ bar: 'baz'
+ }
+ end
+ let(:query) { base_query }
before do
- allow(value).to receive(:to_query).and_return(value)
-
allow(subject).to receive(:header).and_return(value)
- allow(subject).to receive(:params).and_return(value)
- allow(subject).to receive(:request).and_return(value)
+ allow(subject).to receive(:params).and_return(query)
+ allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}"))
end
context 'when resource can be paginated' do
@@ -28,10 +36,7 @@ describe API::Helpers::Pagination do
end
describe 'first page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2 })
- end
+ let(:query) { base_query.merge(per_page: 2) }
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 2
@@ -43,7 +48,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[1].id}&pagination=keyset&per_page=2")
+ expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -54,10 +59,7 @@ describe API::Helpers::Pagination do
end
describe 'second page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[1].id })
- end
+ let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[1].id) }
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 1
@@ -69,7 +71,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[2].id}&pagination=keyset&per_page=2")
+ expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -80,10 +82,7 @@ describe API::Helpers::Pagination do
end
describe 'third page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2, ks_prev_id: projects[2].id })
- end
+ let(:query) { base_query.merge(per_page: 2, ks_prev_id: projects[2].id) }
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 0
@@ -91,6 +90,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
+ expect_no_header('X-Next-Page')
expect(subject).not_to receive(:header).with('Link')
subject.paginate(resource)
@@ -99,10 +99,7 @@ describe API::Helpers::Pagination do
context 'if order' do
context 'is not present' do
- before do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2 })
- end
+ let(:query) { base_query.merge(per_page: 2) }
it 'is not present it adds default order(:id) desc' do
resource.order_values = []
@@ -144,9 +141,7 @@ describe API::Helpers::Pagination do
# (key is the id)
end
- it 'it also orders by primary key' do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2 })
+ it 'also orders by primary key' do
paginated_relation = subject.paginate(resource)
expect(paginated_relation.order_values).to be_present
@@ -157,46 +152,45 @@ describe API::Helpers::Pagination do
expect(paginated_relation.order_values.second.expr.name).to eq :id
end
- it 'it returns the right records (first page)' do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', per_page: 2 })
+ it 'returns the right records (first page)' do
result = subject.paginate(resource)
expect(result.first).to eq(projects[1])
expect(result.second).to eq(projects[3])
end
- it 'it returns the right records (second page)' do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
- result = subject.paginate(resource)
+ describe 'second page' do
+ let(:query) { base_query.merge(ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2) }
- expect(result.first).to eq(projects[2])
- expect(result.second).to eq(projects[6])
- end
+ it 'returns the right records (second page)' do
+ result = subject.paginate(resource)
- it 'it returns the right records (third page), note increased per_page' do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5 })
- result = subject.paginate(resource)
+ expect(result.first).to eq(projects[2])
+ expect(result.second).to eq(projects[6])
+ end
- expect(result.size).to eq(3)
- expect(result.first).to eq(projects[0])
- expect(result.second).to eq(projects[4])
- expect(result.last).to eq(projects[5])
+ it 'returns the right link to the next page' do
+ expect_header('X-Per-Page', '2')
+ expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
+ expect_header('Link', anything) do |_key, val|
+ expect(val).to include('rel="next"')
+ end
+
+ subject.paginate(resource)
+ end
end
- it 'it returns the right link to the next page' do
- allow(subject).to receive(:params)
- .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+ describe 'third page' do
+ let(:query) { base_query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name, per_page: 5) }
- expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2")
- expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="next"')
- end
+ it 'returns the right records (third page), note increased per_page' do
+ result = subject.paginate(resource)
- subject.paginate(resource)
+ expect(result.size).to eq(3)
+ expect(result.first).to eq(projects[0])
+ expect(result.second).to eq(projects[4])
+ expect(result.last).to eq(projects[5])
+ end
end
end
end
@@ -205,25 +199,13 @@ describe API::Helpers::Pagination do
describe '#paginate (default offset-based pagination)' do
let(:value) { spy('return value') }
+ let(:base_query) { { foo: 'bar', bar: 'baz' } }
+ let(:query) { base_query }
before do
- allow(value).to receive(:to_query).and_return(value)
-
allow(subject).to receive(:header).and_return(value)
- allow(subject).to receive(:params).and_return(value)
- allow(subject).to receive(:request).and_return(value)
- end
-
- describe 'required instance methods' do
- let(:return_spy) { spy }
-
- it 'requires some instance methods' do
- expect_message(:header)
- expect_message(:params)
- expect_message(:request)
-
- subject.paginate(resource)
- end
+ allow(subject).to receive(:params).and_return(query)
+ allow(subject).to receive(:request).and_return(double(url: "#{incoming_api_projects_url}?#{query.to_query}"))
end
context 'when resource can be paginated' do
@@ -232,11 +214,6 @@ describe API::Helpers::Pagination do
end
describe 'first page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ page: 1, per_page: 2 })
- end
-
shared_examples 'response with pagination headers' do
it 'adds appropriate headers' do
expect_header('X-Total', '3')
@@ -247,9 +224,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="first"')
- expect(val).to include('rel="last"')
- expect(val).to include('rel="next"')
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="prev"')
end
@@ -267,6 +244,8 @@ describe API::Helpers::Pagination do
end
end
+ let(:query) { base_query.merge(page: 1, per_page: 2) }
+
context 'when the api_kaminari_count_with_limit feature flag is unset' do
it_behaves_like 'paginated response'
it_behaves_like 'response with pagination headers'
@@ -311,9 +290,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="first"')
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="last"')
- expect(val).to include('rel="next"')
expect(val).not_to include('rel="prev"')
end
@@ -324,10 +303,7 @@ describe API::Helpers::Pagination do
end
describe 'second page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ page: 2, per_page: 2 })
- end
+ let(:query) { base_query.merge(page: 2, per_page: 2) }
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 1
@@ -342,9 +318,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '1')
expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="first"')
- expect(val).to include('rel="last"')
- expect(val).to include('rel="prev"')
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
expect(val).not_to include('rel="next"')
end
@@ -376,10 +352,7 @@ describe API::Helpers::Pagination do
context 'when resource empty' do
describe 'first page' do
- before do
- allow(subject).to receive(:params)
- .and_return({ page: 1, per_page: 2 })
- end
+ let(:query) { base_query.merge(page: 1, per_page: 2) }
it 'returns appropriate amount of resources' do
expect(subject.paginate(resource).count).to eq 0
@@ -394,8 +367,8 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include('rel="first"')
- expect(val).to include('rel="last"')
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
expect(val).not_to include('rel="prev"')
expect(val).not_to include('rel="next"')
expect(val).not_to include('page=0')
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index a0270d93d50..43222ddb5e2 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -121,6 +121,42 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
end
end
+ context "youtrack project" do
+ let(:project) { create(:youtrack_project) }
+
+ before do
+ project.update!(issues_enabled: false)
+ end
+
+ context "with right markdown" do
+ let(:issue) { ExternalIssue.new("YT-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with underscores in the prefix" do
+ let(:issue) { ExternalIssue.new("PRJ_1-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with lowercase letters in the prefix" do
+ let(:issue) { ExternalIssue.new("YTkPrj-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with a single-letter prefix" do
+ let(:issue) { ExternalIssue.new("T-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+ end
+
context "jira project" do
let(:project) { create(:jira_project) }
let(:reference) { issue.to_reference }
diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb
index 2e50e4e2351..c6dcb4e46fd 100644
--- a/spec/lib/banzai/filter/footnote_filter_spec.rb
+++ b/spec/lib/banzai/filter/footnote_filter_spec.rb
@@ -11,6 +11,7 @@ describe Banzai::Filter::FootnoteFilter do
let(:footnote) do
<<~EOF
<p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p>
+ <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p>
<ol>
<li id="fn1">
<p>one <a href="#fnref1">↩</a></p>
@@ -25,6 +26,7 @@ describe Banzai::Filter::FootnoteFilter do
let(:filtered_footnote) do
<<~EOF
<p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p>
+ <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p>
<section class="footnotes"><ol>
<li id="fn1-#{identifier}">
<p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p>
diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb
index b021e69800a..4f0d57ca8a6 100644
--- a/spec/lib/bitbucket_server/client_spec.rb
+++ b/spec/lib/bitbucket_server/client_spec.rb
@@ -64,7 +64,7 @@ describe BitbucketServer::Client do
let(:url) { "#{base_uri}rest/api/1.0/projects/SOME-PROJECT/repos/my-repo/branches" }
it 'requests Bitbucket to create a branch' do
- stub_request(:post, url).to_return(status: 204, headers: headers, body: '{}')
+ stub_request(:post, url).to_return(status: 204, headers: headers, body: nil)
subject.create_branch(project, repo_slug, branch, sha)
@@ -78,7 +78,7 @@ describe BitbucketServer::Client do
let(:url) { "#{base_uri}rest/branch-utils/1.0/projects/SOME-PROJECT/repos/my-repo/branches" }
it 'requests Bitbucket to create a branch' do
- stub_request(:delete, url).to_return(status: 204, headers: headers, body: '{}')
+ stub_request(:delete, url).to_return(status: 204, headers: headers, body: nil)
subject.delete_branch(project, repo_slug, branch, sha)
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index c96e7ab8495..3496b01ebcc 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do
let(:request) { build_request('foo', 'bar') }
it { expect(subject.matches?(request)).to be_falsey }
+
+ context 'existence_check is false' do
+ it { expect(subject.matches?(request, existence_check: false)).to be_truthy }
+ end
end
context "project id ending with .git" do
diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb
index 630732614b2..a7163048370 100644
--- a/spec/lib/feature_spec.rb
+++ b/spec/lib/feature_spec.rb
@@ -186,13 +186,14 @@ describe Feature do
describe Feature::Target do
describe '#targets' do
let(:project) { create(:project) }
+ let(:group) { create(:group) }
let(:user_name) { project.owner.username }
- subject { described_class.new(user: user_name, project: project.full_path) }
+ subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) }
it 'returns all found targets' do
expect(subject.targets).to be_an(Array)
- expect(subject.targets).to eq([project.owner, project])
+ expect(subject.targets).to eq([project.owner, project, group])
end
end
end
diff --git a/spec/lib/gitlab/background_migration/digest_column_spec.rb b/spec/lib/gitlab/background_migration/digest_column_spec.rb
index 3e107ac3027..a25dcb06005 100644
--- a/spec/lib/gitlab/background_migration/digest_column_spec.rb
+++ b/spec/lib/gitlab/background_migration/digest_column_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913
it 'erases token' do
expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.to(
- change { PersonalAccessToken.find(1).token }.from('token-01').to(nil))
+ change { PersonalAccessToken.find(1).read_attribute(:token) }.from('token-01').to(nil))
end
end
@@ -39,7 +39,7 @@ describe Gitlab::BackgroundMigration::DigestColumn, :migration, schema: 20180913
it 'leaves token empty' do
expect { subject.perform(PersonalAccessToken, :token, :token_digest, 1, 2) }.not_to(
- change { PersonalAccessToken.find(1).token }.from(nil))
+ change { PersonalAccessToken.find(1).read_attribute(:token) }.from(nil))
end
end
end
diff --git a/spec/lib/gitlab/chat/command_spec.rb b/spec/lib/gitlab/chat/command_spec.rb
new file mode 100644
index 00000000000..46d23ab2b62
--- /dev/null
+++ b/spec/lib/gitlab/chat/command_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Command do
+ let(:chat_name) { create(:chat_name) }
+
+ let(:command) do
+ described_class.new(
+ project: project,
+ chat_name: chat_name,
+ name: 'spinach',
+ arguments: 'foo',
+ channel: '123',
+ response_url: 'http://example.com'
+ )
+ end
+
+ describe '#try_create_pipeline' do
+ let(:project) { create(:project) }
+
+ it 'returns nil when the command is not valid' do
+ expect(command)
+ .to receive(:valid?)
+ .and_return(false)
+
+ expect(command.try_create_pipeline).to be_nil
+ end
+
+ it 'tries to create the pipeline when a command is valid' do
+ expect(command)
+ .to receive(:valid?)
+ .and_return(true)
+
+ expect(command)
+ .to receive(:create_pipeline)
+
+ command.try_create_pipeline
+ end
+ end
+
+ describe '#create_pipeline' do
+ let(:project) { create(:project, :test_repo) }
+ let(:pipeline) { command.create_pipeline }
+
+ before do
+ stub_repository_ci_yaml_file(sha: project.commit.id)
+
+ project.add_developer(chat_name.user)
+ end
+
+ it 'creates the pipeline' do
+ expect(pipeline).to be_persisted
+ end
+
+ it 'creates the chat data for the pipeline' do
+ expect(pipeline.chat_data).to be_an_instance_of(Ci::PipelineChatData)
+ end
+
+ it 'stores the chat name ID in the chat data' do
+ expect(pipeline.chat_data.chat_name_id).to eq(chat_name.id)
+ end
+
+ it 'stores the response URL in the chat data' do
+ expect(pipeline.chat_data.response_url).to eq('http://example.com')
+ end
+
+ it 'creates the environment variables for the pipeline' do
+ vars = pipeline.variables.each_with_object({}) do |row, hash|
+ hash[row.key] = row.value
+ end
+
+ expect(vars['CHAT_INPUT']).to eq('foo')
+ expect(vars['CHAT_CHANNEL']).to eq('123')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat/output_spec.rb b/spec/lib/gitlab/chat/output_spec.rb
new file mode 100644
index 00000000000..b179f9e9d0a
--- /dev/null
+++ b/spec/lib/gitlab/chat/output_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Output do
+ let(:build) do
+ create(:ci_build, pipeline: create(:ci_pipeline, source: :chat))
+ end
+
+ let(:output) { described_class.new(build) }
+
+ describe '#to_s' do
+ it 'returns the build output as a String' do
+ trace = Gitlab::Ci::Trace.new(build)
+
+ trace.set("echo hello\nhello")
+
+ allow(build)
+ .to receive(:trace)
+ .and_return(trace)
+
+ allow(output)
+ .to receive(:read_offset_and_length)
+ .and_return([0, 13])
+
+ expect(output.to_s).to eq('he')
+ end
+ end
+
+ describe '#read_offset_and_length' do
+ context 'without the chat_reply trace section' do
+ it 'falls back to using the build_script trace section' do
+ expect(output)
+ .to receive(:find_build_trace_section)
+ .with('chat_reply')
+ .and_return(nil)
+
+ expect(output)
+ .to receive(:find_build_trace_section)
+ .with('build_script')
+ .and_return({ name: 'build_script', byte_start: 1, byte_end: 4 })
+
+ expect(output.read_offset_and_length).to eq([1, 3])
+ end
+ end
+
+ context 'without the build_script trace section' do
+ it 'raises MissingBuildSectionError' do
+ expect { output.read_offset_and_length }
+ .to raise_error(described_class::MissingBuildSectionError)
+ end
+ end
+
+ context 'with the chat_reply trace section' do
+ it 'returns the read offset and length as an Array' do
+ trace = Gitlab::Ci::Trace.new(build)
+
+ allow(build)
+ .to receive(:trace)
+ .and_return(trace)
+
+ allow(trace)
+ .to receive(:extract_sections)
+ .and_return([{ name: 'chat_reply', byte_start: 1, byte_end: 4 }])
+
+ expect(output.read_offset_and_length).to eq([1, 3])
+ end
+ end
+ end
+
+ describe '#without_executed_command_line' do
+ it 'returns the input without the first line' do
+ expect(output.without_executed_command_line("hello\nworld"))
+ .to eq('world')
+ end
+
+ it 'returns an empty String when the input is empty' do
+ expect(output.without_executed_command_line('')).to eq('')
+ end
+
+ it 'returns an empty String when the input consits of a single newline' do
+ expect(output.without_executed_command_line("\n")).to eq('')
+ end
+ end
+
+ describe '#find_build_trace_section' do
+ it 'returns nil when no section could be found' do
+ expect(output.find_build_trace_section('foo')).to be_nil
+ end
+
+ it 'returns the trace section when it could be found' do
+ section = { name: 'chat_reply', byte_start: 1, byte_end: 4 }
+
+ allow(output)
+ .to receive(:trace_sections)
+ .and_return([section])
+
+ expect(output.find_build_trace_section('chat_reply')).to eq(section)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat/responder/base_spec.rb b/spec/lib/gitlab/chat/responder/base_spec.rb
new file mode 100644
index 00000000000..7fa9bad9d38
--- /dev/null
+++ b/spec/lib/gitlab/chat/responder/base_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Responder::Base do
+ let(:project) { double(:project) }
+ let(:pipeline) { double(:pipeline, project: project) }
+ let(:build) { double(:build, pipeline: pipeline) }
+ let(:responder) { described_class.new(build) }
+
+ describe '#pipeline' do
+ it 'returns the pipeline' do
+ expect(responder.pipeline).to eq(pipeline)
+ end
+ end
+
+ describe '#project' do
+ it 'returns the project' do
+ expect(responder.project).to eq(project)
+ end
+ end
+
+ describe '#success' do
+ it 'raises NotImplementedError' do
+ expect { responder.success }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#failure' do
+ it 'raises NotImplementedError' do
+ expect { responder.failure }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#send_response' do
+ it 'raises NotImplementedError' do
+ expect { responder.send_response('hello') }
+ .to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#scheduled_output' do
+ it 'raises NotImplementedError' do
+ expect { responder.scheduled_output }
+ .to raise_error(NotImplementedError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat/responder/slack_spec.rb b/spec/lib/gitlab/chat/responder/slack_spec.rb
new file mode 100644
index 00000000000..a1553232b32
--- /dev/null
+++ b/spec/lib/gitlab/chat/responder/slack_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Responder::Slack do
+ let(:chat_name) { create(:chat_name, chat_id: 'U123') }
+
+ let(:pipeline) do
+ pipeline = create(:ci_pipeline)
+
+ pipeline.create_chat_data!(
+ response_url: 'http://example.com',
+ chat_name_id: chat_name.id
+ )
+
+ pipeline
+ end
+
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:responder) { described_class.new(build) }
+
+ describe '#send_response' do
+ it 'sends a response back to Slack' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ 'http://example.com',
+ { headers: { Accept: 'application/json' }, body: 'hello'.to_json }
+ )
+
+ responder.send_response('hello')
+ end
+ end
+
+ describe '#success' do
+ it 'returns the output for a successful build' do
+ expect(responder)
+ .to receive(:send_response)
+ .with(hash_including(text: /<@U123>:.+hello/, response_type: :in_channel))
+
+ responder.success('hello')
+ end
+
+ it 'limits the output to a fixed size' do
+ expect(responder)
+ .to receive(:send_response)
+ .with(hash_including(text: /The output is too large/))
+
+ responder.success('a' * 4000)
+ end
+
+ it 'does not send a response if the output is empty' do
+ expect(responder).not_to receive(:send_response)
+
+ responder.success('')
+ end
+ end
+
+ describe '#failure' do
+ it 'returns the output for a failed build' do
+ expect(responder).to receive(:send_response).with(
+ hash_including(
+ text: /<@U123>:.+Sorry, the build failed!/,
+ response_type: :in_channel
+ )
+ )
+
+ responder.failure
+ end
+ end
+
+ describe '#scheduled_output' do
+ it 'returns the output for a scheduled build' do
+ output = responder.scheduled_output
+
+ expect(output).to eq({ text: '' })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat/responder_spec.rb b/spec/lib/gitlab/chat/responder_spec.rb
new file mode 100644
index 00000000000..9893689cba9
--- /dev/null
+++ b/spec/lib/gitlab/chat/responder_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Chat::Responder do
+ describe '.responder_for' do
+ context 'using a regular build' do
+ it 'returns nil' do
+ build = create(:ci_build)
+
+ expect(described_class.responder_for(build)).to be_nil
+ end
+ end
+
+ context 'using a chat build' do
+ it 'returns the responder for the build' do
+ pipeline = create(:ci_pipeline)
+ build = create(:ci_build, pipeline: pipeline)
+ service = double(:service, chat_responder: Gitlab::Chat::Responder::Slack)
+ chat_name = double(:chat_name, service: service)
+ chat_data = double(:chat_data, chat_name: chat_name)
+
+ allow(pipeline)
+ .to receive(:chat_data)
+ .and_return(chat_data)
+
+ expect(described_class.responder_for(build))
+ .to be_an_instance_of(Gitlab::Chat::Responder::Slack)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/chat_spec.rb b/spec/lib/gitlab/chat_spec.rb
new file mode 100644
index 00000000000..d61c4b36668
--- /dev/null
+++ b/spec/lib/gitlab/chat_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Chat, :use_clean_rails_memory_store_caching do
+ describe '.available?' do
+ it 'returns true when the chatops feature is available' do
+ allow(Feature)
+ .to receive(:enabled?)
+ .with(:chatops, default_enabled: true)
+ .and_return(true)
+
+ expect(described_class).to be_available
+ end
+
+ it 'returns false when the chatops feature is not available' do
+ allow(Feature)
+ .to receive(:enabled?)
+ .with(:chatops, default_enabled: true)
+ .and_return(false)
+
+ expect(described_class).not_to be_available
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index fab071405df..c9d1d09a938 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -101,6 +101,8 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
checkout_sha: project.commit.id,
after_sha: nil,
before_sha: nil,
+ source_sha: merge_request.diff_head_sha,
+ target_sha: merge_request.target_branch_sha,
trigger_request: nil,
schedule: nil,
merge_request: merge_request,
@@ -118,5 +120,10 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
expect(pipeline).to be_merge_request
expect(pipeline.merge_request).to eq(merge_request)
end
+
+ it 'correctly sets souce sha and target sha to pipeline' do
+ expect(pipeline.source_sha).to eq(merge_request.diff_head_sha)
+ expect(pipeline.target_sha).to eq(merge_request.target_branch_sha)
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 6aa802ce6fd..dab0fb51bcc 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -161,6 +161,54 @@ describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
+ describe '#source_sha' do
+ subject { command.source_sha }
+
+ let(:command) do
+ described_class.new(project: project,
+ source_sha: source_sha,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+
+ let(:source_sha) { nil }
+
+ context 'when source_sha is specified' do
+ let(:source_sha) { 'abc' }
+
+ it 'returns the specified value' do
+ is_expected.to eq('abc')
+ end
+ end
+ end
+
+ describe '#target_sha' do
+ subject { command.target_sha }
+
+ let(:command) do
+ described_class.new(project: project,
+ target_sha: target_sha,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+
+ let(:target_sha) { nil }
+
+ context 'when target_sha is specified' do
+ let(:target_sha) { 'abc' }
+
+ it 'returns the specified value' do
+ is_expected.to eq('abc')
+ end
+ end
+ end
+
describe '#protected_ref?' do
let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb
new file mode 100644
index 00000000000..7c1c016b4bb
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/remove_unwanted_chat_jobs_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::RemoveUnwantedChatJobs do
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ build(:ci_pipeline_with_one_job, project: project, ref: 'master')
+ end
+
+ let(:command) do
+ double(:command, project: project, chat_data: { command: 'echo' })
+ end
+
+ describe '#perform!' do
+ it 'removes unwanted jobs for chat pipelines' do
+ allow(pipeline).to receive(:chat?).and_return(true)
+
+ pipeline.config_processor.jobs[:echo] = double(:job)
+
+ described_class.new(pipeline, command).perform!
+
+ expect(pipeline.config_processor.jobs.keys).to eq([:echo])
+ end
+ end
+
+ it 'does not remove any jobs for non-chat pipelines' do
+ described_class.new(pipeline, command).perform!
+
+ expect(pipeline.config_processor.jobs.keys).to eq([:rspec])
+ end
+end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 75080aacd96..00cb1e6446a 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -184,7 +184,7 @@ describe Gitlab::Danger::Helper do
describe '#changes_by_category' do
it 'categorizes changed files' do
- expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo] }
+ expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] }
allow(fake_git).to receive(:modified_files) { [] }
allow(fake_git).to receive(:renamed_files) { [] }
@@ -193,6 +193,7 @@ describe Gitlab::Danger::Helper do
database: %w[db/foo],
docs: %w[foo.md],
frontend: %w[foo.js],
+ none: %w[ee/changelogs/foo.yml],
qa: %w[qa/foo],
unknown: %w[foo]
)
@@ -216,6 +217,7 @@ describe Gitlab::Danger::Helper do
'app/views/foo' | :frontend
'public/foo' | :frontend
'spec/javascripts/foo' | :frontend
+ 'spec/frontend/bar' | :frontend
'vendor/assets/foo' | :frontend
'jest.config.js' | :frontend
'package.json' | :frontend
@@ -224,6 +226,7 @@ describe Gitlab::Danger::Helper do
'ee/app/assets/foo' | :frontend
'ee/app/views/foo' | :frontend
'ee/spec/javascripts/foo' | :frontend
+ 'ee/spec/frontend/bar' | :frontend
'app/models/foo' | :backend
'bin/foo' | :backend
@@ -244,12 +247,13 @@ describe Gitlab::Danger::Helper do
'vendor/languages.yml' | :backend
'vendor/licenses.csv' | :backend
- 'Dangerfile' | :backend
- 'Gemfile' | :backend
- 'Gemfile.lock' | :backend
- 'Procfile' | :backend
- 'Rakefile' | :backend
- 'FOO_VERSION' | :backend
+ 'Dangerfile' | :backend
+ 'Gemfile' | :backend
+ 'Gemfile.lock' | :backend
+ 'Procfile' | :backend
+ 'Rakefile' | :backend
+ '.gitlab-ci.yml' | :backend
+ 'FOO_VERSION' | :backend
'ee/FOO_VERSION' | :unknown
@@ -259,6 +263,9 @@ describe Gitlab::Danger::Helper do
'ee/db/foo' | :database
'ee/qa/foo' | :qa
+ 'changelogs/foo' | :none
+ 'ee/changelogs/foo' | :none
+
'FOO' | :unknown
'foo' | :unknown
@@ -282,6 +289,7 @@ describe Gitlab::Danger::Helper do
:docs | '~Documentation'
:foo | '~foo'
:frontend | '~frontend'
+ :none | ''
:qa | '~QA'
end
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
index 4d222564fd0..0ebd8994636 100644
--- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the module name' do
- expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
+ it 'does not link the module name' do
+ expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
end
it 'links the homepage' do
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
index a97803b119e..f00f6b47b94 100644
--- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do
end
it 'links dependencies' do
- expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
- expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
- expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
end
+ it 'links to external dependencies' do
+ expect(subject).to include(link('rails', 'https://github.com/rails/rails'))
+ expect(subject).to include(link('responders', 'https://github.com/rails/responders'))
+ expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets'))
+ end
+
it 'links GitHub repos' do
expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
index 24ad7d12f4c..6c6a5d70576 100644
--- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
@@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the gem name' do
- expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
+ it 'does not link the gem name' do
+ expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
end
it 'links the license' do
diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
index 1e8b72afb7b..9050127af7f 100644
--- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
"express": "4.2.x",
"bigpipe": "bigpipe/pagelet",
"plates": "https://github.com/flatiron/plates/tarball/master",
- "karma": "^1.4.1"
+ "karma": "^1.4.1",
+ "random": "git+https://EdOverflow@github.com/example/example.git"
},
"devDependencies": {
"vows": "^0.7.0",
@@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the module name' do
- expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name'))
+ it 'does not link the module name' do
+ expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name'))
end
it 'links the homepage' do
@@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
expect(subject).to include(link('primus', 'https://npmjs.com/package/primus'))
expect(subject).to include(link('async', 'https://npmjs.com/package/async'))
expect(subject).to include(link('express', 'https://npmjs.com/package/express'))
- expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe'))
- expect(subject).to include(link('plates', 'https://npmjs.com/package/plates'))
expect(subject).to include(link('karma', 'https://npmjs.com/package/karma'))
expect(subject).to include(link('vows', 'https://npmjs.com/package/vows'))
expect(subject).to include(link('assume', 'https://npmjs.com/package/assume'))
expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit'))
end
+ it 'links dependencies to URL detected on value' do
+ expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet'))
+ expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master'))
+ end
+
+ it 'does not link to NPM when invalid git URL' do
+ expect(subject).not_to include(link('random', 'https://npmjs.com/package/random'))
+ end
+
it 'links GitHub repos' do
expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet'))
end
diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
new file mode 100644
index 00000000000..f81dbcf62da
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
@@ -0,0 +1,42 @@
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::Parser::Gemfile do
+ describe '#parse' do
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ source 'https://rubygems.org'
+
+ gem "rails", '4.2.6', github: "rails/rails"
+ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+ gem 'responders', '~> 2.0', :github => 'rails/responders'
+ gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
+ gem 'default_value_for', '~> 3.0.0'
+ CONTENT
+ end
+
+ subject { described_class.new(file_content).parse(keyword: 'gem') }
+
+ def fetch_package(name)
+ subject.find { |package| package.name == name }
+ end
+
+ it 'returns parsed packages' do
+ expect(subject.size).to eq(5)
+ expect(subject).to all(be_a(Gitlab::DependencyLinker::Package))
+ end
+
+ it 'packages respond to name and external_ref accordingly' do
+ expect(fetch_package('rails')).to have_attributes(name: 'rails',
+ github_ref: 'rails/rails',
+ git_ref: nil)
+
+ expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets',
+ github_ref: nil,
+ git_ref: 'https://gitlab.example.com/gems/sprockets')
+
+ expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for',
+ github_ref: nil,
+ git_ref: nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
index cdfd7ad9826..8f1b523653e 100644
--- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
@@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do
it 'links packages' do
expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
- expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar'))
+ end
+
+ it 'links external packages' do
+ expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git'))
end
it 'links Git repos' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
index ed60ab45955..bacec830103 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
@@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the gem name' do
- expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
+ it 'does not link the pod name' do
+ expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
end
it 'links the license' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 8a9e78ba3c3..b3a728c139e 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1704,6 +1704,37 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#merge_to_ref' do
+ let(:repository) { mutable_repository }
+ let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:right_branch) { 'test-master' }
+ let(:target_ref) { 'refs/merge-requests/999/merge' }
+
+ before do
+ repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch)
+ end
+
+ def merge_to_ref
+ repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message')
+ end
+
+ it 'generates a commit in the target_ref' do
+ expect(repository.ref_exists?(target_ref)).to be(false)
+
+ commit_sha = merge_to_ref
+ ref_head = repository.commit(target_ref)
+
+ expect(commit_sha).to be_present
+ expect(repository.ref_exists?(target_ref)).to be(true)
+ expect(ref_head.id).to eq(commit_sha)
+ end
+
+ it 'does not change the right branch HEAD' do
+ expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target }
+ end
+ end
+
describe '#merge' do
let(:repository) { mutable_repository }
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index 95bf7685ade..13cf52fd795 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -100,4 +100,22 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
expect { fake_class.new.find_object }.to raise_error(/Implement #find_object in #{fake_class.name}/)
end
end
+
+ describe '#authorize' do
+ it 'adds permissions from subclasses to those of superclasses when used on classes' do
+ base_class = Class.new do
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :base_authorization
+ end
+
+ sub_class = Class.new(base_class) do
+ authorize :sub_authorization
+ end
+
+ expect(base_class.required_permissions).to contain_exactly(:base_authorization)
+ expect(sub_class.required_permissions)
+ .to contain_exactly(:base_authorization, :sub_authorization)
+ end
+ end
end
diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
new file mode 100644
index 00000000000..cf3a8bcc8b4
--- /dev/null
+++ b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Authorize::Instrumentation do
+ describe '#build_checker' do
+ let(:current_user) { double(:current_user) }
+ let(:abilities) { [double(:first_ability), double(:last_ability)] }
+
+ let(:checker) do
+ described_class.new.__send__(:build_checker, current_user, abilities)
+ end
+
+ it 'returns a checker which checks for a single object' do
+ object = double(:object)
+
+ abilities.each do |ability|
+ spy_ability_check_for(ability, object, passed: true)
+ end
+
+ expect(checker.call(object)).to eq(object)
+ end
+
+ it 'returns a checker which checks for all objects' do
+ objects = [double(:first), double(:last)]
+
+ abilities.each do |ability|
+ objects.each do |object|
+ spy_ability_check_for(ability, object, passed: true)
+ end
+ end
+
+ expect(checker.call(objects)).to eq(objects)
+ end
+
+ context 'when some objects would not pass the check' do
+ it 'returns nil when it is single object' do
+ disallowed = double(:object)
+
+ spy_ability_check_for(abilities.first, disallowed, passed: false)
+
+ expect(checker.call(disallowed)).to be_nil
+ end
+
+ it 'returns only objects which passed when there are more than one' do
+ allowed = double(:allowed)
+ disallowed = double(:disallowed)
+
+ spy_ability_check_for(abilities.first, disallowed, passed: false)
+
+ abilities.each do |ability|
+ spy_ability_check_for(ability, allowed, passed: true)
+ end
+
+ expect(checker.call([disallowed, allowed]))
+ .to contain_exactly(allowed)
+ end
+ end
+
+ def spy_ability_check_for(ability, object, passed: true)
+ expect(Ability)
+ .to receive(:allowed?)
+ .with(current_user, ability, object)
+ .and_return(passed)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/authorize_spec.rb b/spec/lib/gitlab/graphql/authorize_spec.rb
deleted file mode 100644
index 9c17a3b0e4b..00000000000
--- a/spec/lib/gitlab/graphql/authorize_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Graphql::Authorize do
- describe '#authorize' do
- it 'adds permissions from subclasses to those of superclasses when used on classes' do
- base_class = Class.new do
- extend Gitlab::Graphql::Authorize
-
- authorize :base_authorization
- end
- sub_class = Class.new(base_class) do
- authorize :sub_authorization
- end
-
- expect(base_class.required_permissions).to contain_exactly(:base_authorization)
- expect(sub_class.required_permissions)
- .to contain_exactly(:base_authorization, :sub_authorization)
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index c15b360b563..018a5d3dd3d 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -131,6 +131,7 @@ ci_pipelines:
- merge_request
- deployments
- environments
+- chat_data
pipeline_variables:
- pipeline
stages:
@@ -232,6 +233,7 @@ project:
- pushover_service
- jira_service
- redmine_service
+- youtrack_service
- custom_issue_tracker_service
- bugzilla_service
- gitlab_issue_tracker_service
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 68eaa70e6b6..4b234411a44 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do
expect(parsed_merge_request).to eq(merge_request)
end
+
+ context 'when the merge request has diffs' do
+ let(:merge_request) do
+ build(:merge_request, source_project: forked_project, target_project: project)
+ end
+
+ context 'when the diff is invalid' do
+ let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') }
+
+ it 'sets the diff to nil' do
+ expect(merge_request_diff).to be_invalid
+ expect(merge_request_diff.merge_request).to eq merge_request
+ expect(parsed_merge_request.merge_request_diff).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1327f414498..773651dd226 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6630,6 +6630,26 @@
"deploy_keys": [],
"services": [
{
+ "id": 101,
+ "title": "YouTrack",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.327Z",
+ "updated_at": "2016-06-14T15:01:51.327Z",
+ "active": false,
+ "properties": {},
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "job_events": true,
+ "type": "YoutrackService",
+ "category": "issue_tracker",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
"id": 100,
"title": "JetBrains TeamCity CI",
"project_id": 5,
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 46fdfba953b..cfc3e0ce926 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -78,6 +78,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['releases']).not_to be_empty
end
+ it 'has no author on releases' do
+ expect(saved_project_json['releases'].first['author']).to be_nil
+ end
+
+ it 'has the author ID on releases' do
+ expect(saved_project_json['releases'].first['author_id']).not_to be_nil
+ end
+
it 'has issues' do
expect(saved_project_json['issues']).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index baca8f6d542..ee96e5c4d42 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -235,6 +235,8 @@ Ci::Pipeline:
- ref
- sha
- before_sha
+- source_sha
+- target_sha
- push_data
- created_at
- updated_at
diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb
index f2d750c6595..2c288cff6ef 100644
--- a/spec/lib/gitlab/import_export/shared_spec.rb
+++ b/spec/lib/gitlab/import_export/shared_spec.rb
@@ -14,6 +14,16 @@ describe Gitlab::ImportExport::Shared do
expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]'])
end
+ it 'updates the import JID' do
+ import_state = create(:import_state, project: project, jid: 'jid-test')
+
+ expect_next_instance_of(Gitlab::Import::Logger) do |logger|
+ expect(logger).to receive(:error).with(hash_including(import_jid: import_state.jid))
+ end
+
+ subject.error(error)
+ end
+
it 'calls the error logger with the full message' do
expect(subject).to receive(:log_error).with(hash_including(message: error.message))
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 27c802f34ec..95b6b3fd953 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should generate the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
- expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.2-kube-1.11.0')
+ expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7')
expect(container.env.count).to eq(3)
expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT])
expect(container.command).to match_array(["/bin/sh"])
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 02364e92149..978e64c4407 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '#initialize' do
+ shared_examples 'local address' do
+ it 'blocks local addresses' do
+ expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+
+ context 'when local requests are allowed' do
+ before do
+ stub_application_setting(allow_local_requests_from_hooks_and_services: true)
+ end
+
+ it 'allows local addresses' do
+ expect { client }.not_to raise_error
+ end
+ end
+ end
+
+ context 'localhost address' do
+ let(:api_url) { 'http://localhost:22' }
+
+ it_behaves_like 'local address'
+ end
+
+ context 'private network address' do
+ let(:api_url) { 'http://192.168.1.2:3003' }
+
+ it_behaves_like 'local address'
+ end
+ end
+
describe '#core_client' do
subject { client.core_client }
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 1ec1ba19e39..8961ecc4be0 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -4,10 +4,8 @@ require 'spec_helper'
describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
describe '#token' do
- shared_examples 'an LFS token generator' do
+ shared_examples 'a valid LFS token' do
it 'returns a computed token' do
- expect(Settings).to receive(:attr_encrypted_db_key_base).and_return('fbnbv6hdjweo53qka7kza8v8swxc413c05pb51qgtfte0bygh1p2e508468hfsn5ntmjcyiz7h1d92ashpet5pkdyejg7g8or3yryhuso4h8o5c73h429d9c3r6bjnet').twice
-
token = lfs_token.token
expect(token).not_to be_nil
@@ -20,11 +18,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
let(:actor) { create(:user, username: 'test_user_lfs_1') }
let(:lfs_token) { described_class.new(actor) }
- before do
- allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO')
- end
-
- it_behaves_like 'an LFS token generator'
+ it_behaves_like 'a valid LFS token'
it 'returns the correct username' do
expect(lfs_token.actor_name).to eq(actor.username)
@@ -40,11 +34,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
let(:actor) { create(:key, user: user) }
let(:lfs_token) { described_class.new(actor) }
- before do
- allow(user).to receive(:encrypted_password).and_return('$2a$04$C1GAMKsOKouEbhKy2JQoe./3LwOfQAZc.hC8zW2u/wt8xgukvnlV.')
- end
-
- it_behaves_like 'an LFS token generator'
+ it_behaves_like 'a valid LFS token'
it 'returns the correct username' do
expect(lfs_token.actor_name).to eq(user.username)
@@ -65,7 +55,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
allow(actor).to receive(:id).and_return(actor_id)
end
- it_behaves_like 'an LFS token generator'
+ it_behaves_like 'a valid LFS token'
it 'returns the correct username' do
expect(lfs_token.actor_name).to eq("lfs+deploy-key-#{actor_id}")
@@ -87,10 +77,6 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
let(:actor) { create(:user, username: 'test_user_lfs_1') }
let(:lfs_token) { described_class.new(actor) }
- before do
- allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO')
- end
-
context 'for an HMAC token' do
before do
# We're not interested in testing LegacyRedisDeviseToken here
@@ -240,4 +226,18 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
end
end
end
+
+ describe '#authentication_payload' do
+ it 'returns a Hash designed for gitlab-shell' do
+ actor = create(:user)
+ lfs_token = described_class.new(actor)
+ repo_http_path = 'http://localhost/user/repo.git'
+ authentication_payload = lfs_token.authentication_payload(repo_http_path)
+
+ expect(authentication_payload[:username]).to eq(actor.username)
+ expect(authentication_payload[:repository_http_path]).to eq(repo_http_path)
+ expect(authentication_payload[:lfs_token]).to be_a String
+ expect(authentication_payload[:expires_in]).to eq(described_class::DEFAULT_EXPIRE_TIME)
+ end
+ end
end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 8bb0c1a0b8a..9f2214f7ce7 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Gitlab::Profiler do
- RSpec::Matchers.define_negated_matcher :not_change, :change
-
let(:null_logger) { Logger.new('/dev/null') }
let(:private_token) { 'private' }
@@ -187,7 +185,7 @@ describe Gitlab::Profiler do
end
it 'does not modify the standard Rails loggers' do
- expect { described_class.with_custom_logger(nil) { } }
+ expect { described_class.with_custom_logger(nil) {} }
.to not_change { ActiveRecord::Base.logger }
.and not_change { ActionController::Base.logger }
.and not_change { ActiveSupport::LogSubscriber.colorize_logging }
@@ -204,7 +202,7 @@ describe Gitlab::Profiler do
end
it 'cleans up ApplicationController afterwards' do
- expect { described_class.with_user(user) { } }
+ expect { described_class.with_user(user) {} }
.to not_change { ActionController.instance_methods(false) }
end
end
@@ -213,7 +211,7 @@ describe Gitlab::Profiler do
it 'does not define methods on ApplicationController' do
expect(ApplicationController).not_to receive(:define_method)
- described_class.with_user(nil) { }
+ described_class.with_user(nil) {}
end
end
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 1cc2bde50e9..0cee100b64e 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -7,11 +7,17 @@ describe Gitlab::ProjectTemplate do
described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'),
described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'),
described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'),
+ described_class.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'),
described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'),
described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'),
described_class.new('plainhtml', 'Pages/Plain HTML', 'Everything you need to get started using a plain HTML Pages site.', 'https://gitlab.com/pages/plain-html'),
described_class.new('gitbook', 'Pages/GitBook', 'Everything you need to get started using a GitBook Pages site.', 'https://gitlab.com/pages/gitbook'),
- described_class.new('hexo', 'Pages/Hexo', 'Everything you need to get started using a plan Hexo Pages site.', 'https://gitlab.com/pages/hexo')
+ described_class.new('hexo', 'Pages/Hexo', 'Everything you need to get started using a Hexo Pages site.', 'https://gitlab.com/pages/hexo'),
+ described_class.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo'),
+ described_class.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll'),
+ described_class.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html'),
+ described_class.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook'),
+ described_class.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo')
]
expect(described_class.all).to be_an(Array)
diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb
index 1bc6536439e..c54be78f050 100644
--- a/spec/lib/gitlab/serializer/pagination_spec.rb
+++ b/spec/lib/gitlab/serializer/pagination_spec.rb
@@ -1,16 +1,12 @@
require 'spec_helper'
describe Gitlab::Serializer::Pagination do
- let(:request) { spy('request') }
+ let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) }
let(:response) { spy('response') }
let(:headers) { spy('headers') }
before do
- allow(request).to receive(:query_parameters)
- .and_return(params)
-
- allow(response).to receive(:headers)
- .and_return(headers)
+ allow(response).to receive(:headers).and_return(headers)
end
let(:pagination) { described_class.new(request, response) }
@@ -19,7 +15,7 @@ describe Gitlab::Serializer::Pagination do
subject { pagination.paginate(resource) }
let(:resource) { User.all }
- let(:params) { { page: 1, per_page: 2 } }
+ let(:query) { { page: 1, per_page: 2 } }
context 'when a multiple resources are present in relation' do
before do
diff --git a/spec/lib/gitlab/slash_commands/application_help_spec.rb b/spec/lib/gitlab/slash_commands/application_help_spec.rb
new file mode 100644
index 00000000000..b203a1ee79c
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/application_help_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::ApplicationHelp do
+ let(:params) { { command: '/gitlab', text: 'help' } }
+
+ describe '#execute' do
+ subject do
+ described_class.new(params).execute
+ end
+
+ it 'displays the help section' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:text]).to include('Available commands')
+ expect(subject[:text]).to include('/gitlab [project name or alias] issue show')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/presenters/error_spec.rb b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb
new file mode 100644
index 00000000000..30ff81510c1
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/presenters/error_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Presenters::Error do
+ subject { described_class.new('Error').message }
+
+ it { is_expected.to be_a(Hash) }
+
+ it 'shows the error message' do
+ expect(subject[:response_type]).to be(:ephemeral)
+ expect(subject[:status]).to eq(200)
+ expect(subject[:text]).to eq('Error')
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/presenters/run_spec.rb b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb
new file mode 100644
index 00000000000..f3ab01ef6bb
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/presenters/run_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Presenters::Run do
+ let(:presenter) { described_class.new }
+
+ describe '#present' do
+ context 'when no builds are present' do
+ it 'returns an error' do
+ builds = double(:builds, take: nil)
+ pipeline = double(:pipeline, builds: builds)
+
+ expect(presenter)
+ .to receive(:unsupported_chat_service)
+
+ presenter.present(pipeline)
+ end
+ end
+
+ context 'when a responder could be found' do
+ it 'returns the output for a scheduled pipeline' do
+ responder = double(:responder, scheduled_output: 'hello')
+ build = double(:build)
+ builds = double(:builds, take: build)
+ pipeline = double(:pipeline, builds: builds)
+
+ allow(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(build)
+ .and_return(responder)
+
+ expect(presenter)
+ .to receive(:in_channel_response)
+ .with('hello')
+
+ presenter.present(pipeline)
+ end
+ end
+
+ context 'when a responder could not be found' do
+ it 'returns an error' do
+ build = double(:build)
+ builds = double(:builds, take: build)
+ pipeline = double(:pipeline, builds: builds)
+
+ allow(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(build)
+ .and_return(nil)
+
+ expect(presenter)
+ .to receive(:unsupported_chat_service)
+
+ presenter.present(pipeline)
+ end
+ end
+ end
+
+ describe '#unsupported_chat_service' do
+ it 'returns an ephemeral response' do
+ expect(presenter)
+ .to receive(:ephemeral_response)
+ .with(text: /Sorry, this chat service is currently not supported/)
+
+ presenter.unsupported_chat_service
+ end
+ end
+
+ describe '#failed_to_schedule' do
+ it 'returns an ephemeral response' do
+ expect(presenter)
+ .to receive(:ephemeral_response)
+ .with(text: /The command could not be scheduled/)
+
+ presenter.failed_to_schedule('foo')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/run_spec.rb b/spec/lib/gitlab/slash_commands/run_spec.rb
new file mode 100644
index 00000000000..900fae05719
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/run_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Run do
+ describe '.available?' do
+ it 'returns true when builds are enabled for the project' do
+ project = double(:project, builds_enabled?: true)
+
+ allow(Gitlab::Chat)
+ .to receive(:available?)
+ .and_return(true)
+
+ expect(described_class.available?(project)).to eq(true)
+ end
+
+ it 'returns false when builds are disabled for the project' do
+ project = double(:project, builds_enabled?: false)
+
+ expect(described_class.available?(project)).to eq(false)
+ end
+
+ it 'returns false when chatops is not available' do
+ allow(Gitlab::Chat)
+ .to receive(:available?)
+ .and_return(false)
+
+ project = double(:project, builds_enabled?: true)
+
+ expect(described_class.available?(project)).to eq(false)
+ end
+ end
+
+ describe '.allowed?' do
+ it 'returns true when the user can create a pipeline' do
+ project = create(:project)
+
+ expect(described_class.allowed?(project, project.creator)).to eq(true)
+ end
+
+ it 'returns false when the user can not create a pipeline' do
+ project = create(:project)
+ user = create(:user)
+
+ expect(described_class.allowed?(project, user)).to eq(false)
+ end
+ end
+
+ describe '#execute' do
+ let(:chat_name) { create(:chat_name) }
+ let(:project) { create(:project) }
+
+ let(:command) do
+ described_class.new(project, chat_name, response_url: 'http://example.com')
+ end
+
+ context 'when a pipeline could not be scheduled' do
+ it 'returns an error' do
+ expect_any_instance_of(Gitlab::Chat::Command)
+ .to receive(:try_create_pipeline)
+ .and_return(nil)
+
+ expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run)
+ .to receive(:failed_to_schedule)
+ .with('foo')
+
+ command.execute(command: 'foo', arguments: '')
+ end
+ end
+
+ context 'when a pipeline could be created but the chat service was not supported' do
+ it 'returns an error' do
+ build = double(:build)
+ pipeline = double(
+ :pipeline,
+ builds: double(:relation, take: build),
+ persisted?: true
+ )
+
+ expect_any_instance_of(Gitlab::Chat::Command)
+ .to receive(:try_create_pipeline)
+ .and_return(pipeline)
+
+ expect(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(build)
+ .and_return(nil)
+
+ expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run)
+ .to receive(:unsupported_chat_service)
+
+ command.execute(command: 'foo', arguments: '')
+ end
+ end
+
+ context 'using a valid pipeline' do
+ it 'schedules the pipeline' do
+ responder = double(:responder, scheduled_output: 'hello')
+ build = double(:build)
+ pipeline = double(
+ :pipeline,
+ builds: double(:relation, take: build),
+ persisted?: true
+ )
+
+ expect_any_instance_of(Gitlab::Chat::Command)
+ .to receive(:try_create_pipeline)
+ .and_return(pipeline)
+
+ expect(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(build)
+ .and_return(responder)
+
+ expect_any_instance_of(Gitlab::SlashCommands::Presenters::Run)
+ .to receive(:in_channel_response)
+ .with(responder.scheduled_output)
+
+ command.execute(command: 'foo', arguments: '')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb
index 2cddc895026..566b5050e47 100644
--- a/spec/lib/gitlab/tracing_spec.rb
+++ b/spec/lib/gitlab/tracing_spec.rb
@@ -44,12 +44,15 @@ describe Gitlab::Tracing do
describe '.tracing_url' do
where(:tracing_url_enabled?, :tracing_url_template, :correlation_id, :process_name, :tracing_url) do
- false | "https://localhost" | "123" | "web" | nil
- true | "https://localhost" | "123" | "web" | "https://localhost"
- true | "https://localhost?service=%{service}" | "123" | "web" | "https://localhost?service=web"
- true | "https://localhost?c=%{correlation_id}" | "123" | "web" | "https://localhost?c=123"
- true | "https://localhost?c=%{correlation_id}&s=%{service}" | "123" | "web" | "https://localhost?c=123&s=web"
- true | "https://localhost?c=%{correlation_id}" | nil | "web" | "https://localhost?c="
+ false | "https://localhost" | "123" | "web" | nil
+ true | "https://localhost" | "123" | "web" | "https://localhost"
+ true | "https://localhost?service={{ service }}" | "123" | "web" | "https://localhost?service=web"
+ true | "https://localhost?c={{ correlation_id }}" | "123" | "web" | "https://localhost?c=123"
+ true | "https://localhost?c={{ correlation_id }}&s={{ service }}" | "123" | "web" | "https://localhost?c=123&s=web"
+ true | "https://localhost?c={{ correlation_id }}" | nil | "web" | "https://localhost?c="
+ true | "https://localhost?c={{ correlation_id }}&s=%22{{ service }}%22" | "123" | "web" | "https://localhost?c=123&s=%22web%22"
+ true | "https://localhost?c={{correlation_id}}&s={{service}}" | "123" | "web" | "https://localhost?c=123&s=web"
+ true | "https://localhost?c={{correlation_id }}&s={{ service}}" | "123" | "web" | "https://localhost?c=123&s=web"
end
with_them do
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index 6fbf60a6222..88e7e2e5ebb 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -36,7 +36,7 @@ describe Sentry::Client do
end
it 'does not follow redirects' do
- expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302')
+ expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response status code: 302')
expect(redirect_req_stub).to have_been_requested
expect(redirected_req_stub).not_to have_been_requested
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4f578c48d5b..418f707a130 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -194,23 +194,53 @@ describe Notify do
let(:new_issue) { create(:issue) }
subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ context 'when a user has permissions to access the new issue' do
+ before do
+ new_issue.project.add_developer(recipient)
+ end
+
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
+
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- it 'contains description about action taken' do
- is_expected.to have_body_text 'Issue was moved to another project'
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
+ end
+ end
+
+ it 'contains the issue title' do
+ is_expected.to have_body_text new_issue.title
+ end
end
- it 'has the correct subject and body' do
- new_issue_url = project_issue_path(new_issue.project, new_issue)
+ context 'when a user does not permissions to access the new issue' do
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(new_issue_url)
- is_expected.to have_body_text(project_issue_path(project, issue))
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.not_to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
+ end
+ end
+
+ it 'does not contain the issue title' do
+ is_expected.not_to have_body_text new_issue.title
+ end
+
+ it 'contains information about missing permissions' do
+ is_expected.to have_body_text "You don't have access to the project."
end
end
end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 129b2f92683..e128fe8a4b7 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -7,7 +7,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
- let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
+ let(:session) do
+ double(:session, { id: '6919a6f1bb119dd7396fadc38fd18d0d',
+ '[]': {} })
+ end
let(:request) do
double(:request, {
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index cc76a2019ec..28d482adebf 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -63,4 +63,19 @@ describe Appearance do
%i(logo header_logo favicon).each do |logo_type|
it_behaves_like 'logo paths', logo_type
end
+
+ describe 'validations' do
+ let(:triplet) { '#000' }
+ let(:hex) { '#AABBCC' }
+
+ it { is_expected.to allow_value(nil).for(:message_background_color) }
+ it { is_expected.to allow_value(triplet).for(:message_background_color) }
+ it { is_expected.to allow_value(hex).for(:message_background_color) }
+ it { is_expected.not_to allow_value('000').for(:message_background_color) }
+
+ it { is_expected.to allow_value(nil).for(:message_font_color) }
+ it { is_expected.to allow_value(triplet).for(:message_font_color) }
+ it { is_expected.to allow_value(hex).for(:message_font_color) }
+ it { is_expected.not_to allow_value('000').for(:message_font_color) }
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 47865e4d08f..17540443688 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -23,6 +23,7 @@ describe Ci::Build do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
+ it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
it { is_expected.to be_a(ArtifactMigratable) }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 460b5c8cd31..ee400bec04b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::Pipeline, :mailer do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
set(:project) { create(:project) }
@@ -22,6 +24,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:auto_canceled_pipelines) }
it { is_expected.to have_many(:auto_canceled_jobs) }
+ it { is_expected.to have_one(:chat_data) }
it { is_expected.to validate_presence_of(:sha) }
it { is_expected.to validate_presence_of(:status) }
@@ -127,6 +130,132 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '.detached_merge_request_pipelines' do
+ subject { described_class.detached_merge_request_pipelines(merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { nil }
+
+ it 'returns detached merge request pipelines' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when target sha exists' do
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#detached_merge_request_pipeline?' do
+ subject { pipeline.detached_merge_request_pipeline? }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { nil }
+
+ it { is_expected.to be_truthy }
+
+ context 'when target sha exists' do
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '.merge_request_pipelines' do
+ subject { described_class.merge_request_pipelines(merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it 'returns merge pipelines' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when target sha is empty' do
+ let(:target_sha) { nil }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#merge_request_pipeline?' do
+ subject { pipeline.merge_request_pipeline? }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it { is_expected.to be_truthy }
+
+ context 'when target sha is empty' do
+ let(:target_sha) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '.mergeable_merge_request_pipelines' do
+ subject { described_class.mergeable_merge_request_pipelines(merge_request) }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it 'returns mergeable merge pipelines' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when target sha does not point the head of the target branch' do
+ let(:target_sha) { merge_request.diff_head_sha }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#mergeable_merge_request_pipeline?' do
+ subject { pipeline.mergeable_merge_request_pipeline? }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it { is_expected.to be_truthy }
+
+ context 'when target sha does not point the head of the target branch' do
+ let(:target_sha) { merge_request.diff_head_sha }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '.merge_request' do
subject { described_class.merge_request }
@@ -397,10 +526,12 @@ describe Ci::Pipeline, :mailer do
'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => pipeline.target_sha.to_s,
'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s)
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s)
end
context 'when source project does not exist' do
@@ -2113,68 +2244,83 @@ describe Ci::Pipeline, :mailer do
describe "#all_merge_requests" do
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') }
-
- it "returns all merge requests having the same source branch" do
- merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
-
- expect(pipeline.all_merge_requests).to eq([merge_request])
- end
- it "doesn't return merge requests having a different source branch" do
- create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
-
- expect(pipeline.all_merge_requests).to be_empty
- end
+ shared_examples 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') }
- context 'when there is a merge request pipeline' do
- let(:source_branch) { 'feature' }
- let(:target_branch) { 'master' }
+ it "returns all merge requests having the same source branch" do
+ merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref)
- let!(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request,
- project: project,
- ref: source_branch,
- merge_request: merge_request)
+ expect(pipeline.all_merge_requests).to eq([merge_request])
end
- let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: source_branch,
- target_project: project,
- target_branch: target_branch)
- end
+ it "doesn't return merge requests having a different source branch" do
+ create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: 'feature', target_branch: 'master')
- it 'returns an associated merge request' do
- expect(pipeline.all_merge_requests).to eq([merge_request])
+ expect(pipeline.all_merge_requests).to be_empty
end
- context 'when there is another merge request pipeline that targets a different branch' do
- let(:target_branch_2) { 'merge-test' }
+ context 'when there is a merge request pipeline' do
+ let(:source_branch) { 'feature' }
+ let(:target_branch) { 'master' }
- let!(:pipeline_2) do
+ let!(:pipeline) do
create(:ci_pipeline,
source: :merge_request,
- project: project,
+ project: pipeline_project,
ref: source_branch,
- merge_request: merge_request_2)
+ merge_request: merge_request)
end
- let(:merge_request_2) do
+ let(:merge_request) do
create(:merge_request,
- source_project: project,
+ source_project: pipeline_project,
source_branch: source_branch,
target_project: project,
- target_branch: target_branch_2)
+ target_branch: target_branch)
+ end
+
+ it 'returns an associated merge request' do
+ expect(pipeline.all_merge_requests).to eq([merge_request])
end
- it 'does not return an associated merge request' do
- expect(pipeline.all_merge_requests).not_to include(merge_request_2)
+ context 'when there is another merge request pipeline that targets a different branch' do
+ let(:target_branch_2) { 'merge-test' }
+
+ let!(:pipeline_2) do
+ create(:ci_pipeline,
+ source: :merge_request,
+ project: pipeline_project,
+ ref: source_branch,
+ merge_request: merge_request_2)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request,
+ source_project: pipeline_project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch_2)
+ end
+
+ it 'does not return an associated merge request' do
+ expect(pipeline.all_merge_requests).not_to include(merge_request_2)
+ end
end
end
end
+
+ it_behaves_like 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline_project) { project }
+ end
+
+ context 'for a fork' do
+ let(:fork) { fork_project(project) }
+
+ it_behaves_like 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline_project) { fork }
+ end
+ end
end
describe '#stuck?' do
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index 64f6d9c8bb4..f16eff92167 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -3,16 +3,17 @@ require 'rails_helper'
describe Clusters::Applications::Helm do
include_examples 'cluster application core specs', :clusters_applications_helm
- describe '.installed' do
- subject { described_class.installed }
+ describe '.available' do
+ subject { described_class.available }
let!(:installed_cluster) { create(:clusters_applications_helm, :installed) }
+ let!(:updated_cluster) { create(:clusters_applications_helm, :updated) }
before do
create(:clusters_applications_helm, :errored)
end
- it { is_expected.to contain_exactly(installed_cluster) }
+ it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
end
describe '#issue_client_cert' do
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 03ca18c6943..d5fd42509a3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -16,18 +16,6 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
- describe '.installed' do
- subject { described_class.installed }
-
- let!(:cluster) { create(:clusters_applications_ingress, :installed) }
-
- before do
- create(:clusters_applications_ingress, :errored)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
describe '#make_installed!' do
before do
application.make_installed!
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index cd29e0d4f53..006b922ab27 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -24,30 +24,6 @@ describe Clusters::Applications::Knative do
it { expect(knative_no_rbac).to be_not_installable }
end
- describe '.installed' do
- subject { described_class.installed }
-
- let!(:cluster) { create(:clusters_applications_knative, :installed) }
-
- before do
- create(:clusters_applications_knative, :errored)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
- describe '#make_installed' do
- subject { described_class.installed }
-
- let!(:cluster) { create(:clusters_applications_knative, :installed) }
-
- before do
- create(:clusters_applications_knative, :errored)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
describe 'make_installed with external_ip' do
before do
application.make_installed!
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index caf59b0fc31..81708b0c2ed 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -9,18 +9,6 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
- describe '.installed' do
- subject { described_class.installed }
-
- let!(:cluster) { create(:clusters_applications_prometheus, :installed) }
-
- before do
- create(:clusters_applications_prometheus, :errored)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
@@ -39,65 +27,6 @@ describe Clusters::Applications::Prometheus do
end
end
- describe '#ready' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, projects: [project]) }
-
- it 'returns true when installed' do
- application = build(:clusters_applications_prometheus, :installed, cluster: cluster)
-
- expect(application).to be_ready
- end
-
- it 'returns false when not_installable' do
- application = build(:clusters_applications_prometheus, :not_installable, cluster: cluster)
-
- expect(application).not_to be_ready
- end
-
- it 'returns false when installable' do
- application = build(:clusters_applications_prometheus, :installable, cluster: cluster)
-
- expect(application).not_to be_ready
- end
-
- it 'returns false when scheduled' do
- application = build(:clusters_applications_prometheus, :scheduled, cluster: cluster)
-
- expect(application).not_to be_ready
- end
-
- it 'returns false when installing' do
- application = build(:clusters_applications_prometheus, :installing, cluster: cluster)
-
- expect(application).not_to be_ready
- end
-
- it 'returns false when errored' do
- application = build(:clusters_applications_prometheus, :errored, cluster: cluster)
-
- expect(application).not_to be_ready
- end
-
- it 'returns true when updating' do
- application = build(:clusters_applications_prometheus, :updating, cluster: cluster)
-
- expect(application).to be_ready
- end
-
- it 'returns true when updated' do
- application = build(:clusters_applications_prometheus, :updated, cluster: cluster)
-
- expect(application).to be_ready
- end
-
- it 'returns true when errored' do
- application = build(:clusters_applications_prometheus, :update_errored, cluster: cluster)
-
- expect(application).to be_ready
- end
- end
-
describe '#prometheus_client' do
context 'cluster is nil' do
it 'returns nil' do
@@ -192,7 +121,7 @@ describe Clusters::Applications::Prometheus do
end
context 'with knative installed' do
- let(:knative) { create(:clusters_applications_knative, :installed ) }
+ let(:knative) { create(:clusters_applications_knative, :updated ) }
let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) }
subject { prometheus.install_command }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 38758ff97bc..6972fc03415 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -11,18 +11,6 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
- describe '.installed' do
- subject { described_class.installed }
-
- let!(:cluster) { create(:clusters_applications_runner, :installed) }
-
- before do
- create(:clusters_applications_runner, :errored)
- end
-
- it { is_expected.to contain_exactly(cluster) }
- end
-
describe '#install_command' do
let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
@@ -34,7 +22,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.1.45')
+ expect(subject.version).to eq('0.2.0')
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -52,7 +40,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.1.45')
+ expect(subject.version).to eq('0.2.0')
end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 92ce2b0999a..3feed4e9718 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -265,12 +265,12 @@ describe Clusters::Cluster do
it { is_expected.to be_valid }
end
- context 'when cluster has an invalid domain' do
- let(:cluster) { build(:cluster, domain: 'not-valid-domain') }
+ context 'when cluster is not a valid hostname' do
+ let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') }
it 'should add an error on domain' do
expect(subject).not_to be_valid
- expect(subject.errors[:domain].first).to eq('is not a fully qualified domain name')
+ expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])')
end
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 4068d98d8f7..3b32ca8df05 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { expect(kubernetes.save).to be_truthy }
end
+
+ context 'when api_url is localhost' do
+ let(:api_url) { 'http://localhost:22' }
+
+ it { expect(kubernetes.save).to be_falsey }
+
+ context 'Application settings allows local requests' do
+ before do
+ allow(ApplicationSetting)
+ .to receive(:current)
+ .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true))
+ end
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
end
context 'when validates token' do
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 12e59b35428..0f5d03ff458 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -12,26 +12,26 @@ describe CommitCollection do
end
end
- describe '.committers' do
+ describe '.authors' do
it 'returns a relation of users when users are found' do
- user = create(:user, email: commit.committer_email.upcase)
+ user = create(:user, email: commit.author_email.upcase)
collection = described_class.new(project, [commit])
- expect(collection.committers).to contain_exactly(user)
+ expect(collection.authors).to contain_exactly(user)
end
- it 'returns empty array when committers cannot be found' do
+ it 'returns empty array when authors cannot be found' do
collection = described_class.new(project, [commit])
- expect(collection.committers).to be_empty
+ expect(collection.authors).to be_empty
end
it 'excludes authors of merge commits' do
commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
- create(:user, email: commit.committer_email.upcase)
+ create(:user, email: commit.author_email.upcase)
collection = described_class.new(project, [commit])
- expect(collection.committers).to be_empty
+ expect(collection.authors).to be_empty
end
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 87bf731340f..4647eecbdef 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -48,7 +48,7 @@ describe Milestone, 'Milestoneish' do
merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone)
merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone)
- merge_requests = milestone.sorted_merge_requests
+ merge_requests = milestone.sorted_merge_requests(member)
expect(merge_requests.first).to eq(merge_request_2)
expect(merge_requests.second).to eq(merge_request_1)
@@ -56,6 +56,51 @@ describe Milestone, 'Milestoneish' do
end
end
+ describe '#merge_requests_visible_to_user' do
+ let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
+
+ context 'when project is private' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'does not return any merge request for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to be_empty
+ end
+
+ it 'returns milestone merge requests for a member' do
+ merge_requests = milestone.merge_requests_visible_to_user(member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
+ end
+
+ context 'when project is public' do
+ context 'when merge requests are available to anyone' do
+ it 'returns milestone merge requests for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
+ end
+
+ context 'when merge requests are available to project members' do
+ before do
+ project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'does not return any merge request for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to be_empty
+ end
+
+ it 'returns milestone merge requests for a member' do
+ merge_requests = milestone.merge_requests_visible_to_user(member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
+ end
+ end
+ end
+
describe '#closed_items_count' do
it 'does not count confidential issues for non project members' do
expect(milestone.closed_items_count(non_member)).to eq 2
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 97a4c212f1c..03ae45e6b17 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -25,7 +25,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
def result
with_reactive_cache do |data|
- data / 2
+ data
end
end
end
@@ -64,7 +64,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
stub_reactive_cache(instance, 4)
end
- it { is_expected.to eq(2) }
+ it { is_expected.to eq(4) }
it 'does not enqueue a background worker' do
expect(ReactiveCachingWorker).not_to receive(:perform_async)
@@ -94,6 +94,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
end
+
+ context 'when cache contains non-nil but blank value' do
+ before do
+ stub_reactive_cache(instance, false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
describe '#clear_reactive_cache!' do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 55d83bc3a6b..40cb4eef60a 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -97,14 +97,31 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
describe PersonalAccessToken, 'TokenAuthenticatable' do
- let(:personal_access_token_name) { 'test-pat-01' }
+ shared_examples 'changes personal access token' do
+ it 'sets new token' do
+ subject
+
+ expect(personal_access_token.token).to eq(token_value)
+ expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(token_value))
+ end
+ end
+
+ shared_examples 'does not change personal access token' do
+ it 'sets new token' do
+ subject
+
+ expect(personal_access_token.token).to be(nil)
+ expect(personal_access_token.token_digest).to eq(token_digest)
+ end
+ end
+
let(:token_value) { 'token' }
+ let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
let(:user) { create(:user) }
let(:personal_access_token) do
- described_class.new(name: personal_access_token_name,
+ described_class.new(name: 'test-pat-01',
user_id: user.id,
scopes: [:api],
- token: token,
token_digest: token_digest)
end
@@ -115,239 +132,71 @@ describe PersonalAccessToken, 'TokenAuthenticatable' do
describe '.find_by_token' do
subject { PersonalAccessToken.find_by_token(token_value) }
- before do
+ it 'finds the token' do
personal_access_token.save
- end
- context 'token_digest already exists' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
-
- it 'finds the token' do
- expect(subject).not_to be_nil
- expect(subject.name).to eql(personal_access_token_name)
- end
- end
-
- context 'token_digest does not exist' do
- let(:token) { token_value }
- let(:token_digest) { nil }
-
- it 'finds the token' do
- expect(subject).not_to be_nil
- expect(subject.name).to eql(personal_access_token_name)
- end
+ expect(subject).to eq(personal_access_token)
end
end
describe '#set_token' do
let(:new_token_value) { 'new-token' }
- subject { personal_access_token.set_token(new_token_value) }
-
- context 'token_digest already exists' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
- it 'overwrites token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(new_token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
- end
- end
-
- context 'token_digest does not exist but token does' do
- let(:token) { token_value }
- let(:token_digest) { nil }
-
- it 'creates new token_digest and clears token' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(new_token_value)
- expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(new_token_value))
- end
- end
-
- context 'token_digest does not exist, nor token' do
- let(:token) { nil }
- let(:token_digest) { nil }
+ subject { personal_access_token.set_token(new_token_value) }
- it 'creates new token_digest' do
- subject
+ it 'sets new token' do
+ subject
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(new_token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(new_token_value))
- end
+ expect(personal_access_token.token).to eq(new_token_value)
+ expect(personal_access_token.token_digest).to eq(Gitlab::CryptoHelper.sha256(new_token_value))
end
end
describe '#ensure_token' do
subject { personal_access_token.ensure_token }
- context 'token_digest already exists' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
-
- it 'does not change token fields' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to be_nil
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
- end
-
- context 'token_digest does not exist but token does' do
- let(:token) { token_value }
+ context 'token_digest does not exist' do
let(:token_digest) { nil }
- it 'does not change token fields' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to eql(token_value)
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to be_nil
- end
+ it_behaves_like 'changes personal access token'
end
- context 'token_digest does not exist, nor token' do
- let(:token) { nil }
- let(:token_digest) { nil }
-
- it 'creates token_digest' do
- subject
+ context 'token_digest already generated' do
+ let(:token_digest) { 's3cr3t' }
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
+ it_behaves_like 'does not change personal access token'
end
end
describe '#ensure_token!' do
subject { personal_access_token.ensure_token! }
- context 'token_digest already exists' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256(token_value) }
-
- it 'does not change token fields' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to be_nil
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
- end
-
- context 'token_digest does not exist but token does' do
- let(:token) { token_value }
+ context 'token_digest does not exist' do
let(:token_digest) { nil }
- it 'does not change token fields' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to eql(token_value)
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to be_nil
- end
+ it_behaves_like 'changes personal access token'
end
- context 'token_digest does not exist, nor token' do
- let(:token) { nil }
- let(:token_digest) { nil }
+ context 'token_digest already generated' do
+ let(:token_digest) { 's3cr3t' }
- it 'creates token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
+ it_behaves_like 'does not change personal access token'
end
end
describe '#reset_token!' do
subject { personal_access_token.reset_token! }
- context 'token_digest already exists' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
-
- it 'creates new token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
- end
-
- context 'token_digest does not exist but token does' do
- let(:token) { 'old-token' }
- let(:token_digest) { nil }
-
- it 'creates new token_digest and clears token' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql(Gitlab::CryptoHelper.sha256(token_value))
- end
- end
-
- context 'token_digest does not exist, nor token' do
- let(:token) { nil }
+ context 'token_digest does not exist' do
let(:token_digest) { nil }
- it 'creates new token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
- end
-
- context 'token_digest exists and newly generated token would be the same' do
- let(:token) { nil }
- let(:token_digest) { Gitlab::CryptoHelper.sha256('old-token') }
-
- before do
- personal_access_token.save
- allow(Devise).to receive(:friendly_token).and_return(
- 'old-token', token_value, 'boom!')
- end
-
- it 'regenerates a new token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
+ it_behaves_like 'changes personal access token'
end
- context 'token exists and newly generated token would be the same' do
- let(:token) { 'old-token' }
- let(:token_digest) { nil }
-
- before do
- personal_access_token.save
- allow(Devise).to receive(:friendly_token).and_return(
- 'old-token', token_value, 'boom!')
- end
+ context 'token_digest already generated' do
+ let(:token_digest) { 's3cr3t' }
- it 'regenerates a new token_digest' do
- subject
-
- expect(personal_access_token.read_attribute('token')).to be_nil
- expect(personal_access_token.token).to eql(token_value)
- expect(personal_access_token.token_digest).to eql( Gitlab::CryptoHelper.sha256(token_value))
- end
+ it_behaves_like 'changes personal access token'
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 2d554326f05..ab1b306e597 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -164,6 +164,28 @@ describe Environment do
end
end
+ describe '#name_without_type' do
+ context 'when it is inside a folder' do
+ subject(:environment) do
+ create(:environment, name: 'staging/review-1')
+ end
+
+ it 'returns name without folder' do
+ expect(environment.name_without_type).to eq 'review-1'
+ end
+ end
+
+ context 'when the environment if a top-level item itself' do
+ subject(:environment) do
+ create(:environment, name: 'production')
+ end
+
+ it 'returns full name' do
+ expect(environment.name_without_type).to eq 'production'
+ end
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index d30228b863c..076ccc96041 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -145,6 +145,24 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
expect(result).to be_nil
end
end
+
+ context 'when sentry client raises exception' do
+ let(:sentry_client) { spy(:sentry_client) }
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(subject).to receive(:sentry_client).and_return(sentry_client)
+ allow(sentry_client).to receive(:list_issues).with(opts)
+ .and_raise(Sentry::Client::Error, 'error message')
+ end
+
+ it 'returns error' do
+ expect(result).to eq(error: 'error message')
+ expect(subject).to have_received(:sentry_client)
+ expect(sentry_client).to have_received(:list_issues)
+ end
+ end
end
describe '#list_sentry_projects' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 5d18e085a6f..6101df2e099 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -765,6 +765,15 @@ describe Issue do
end
end
+ describe '.confidential_only' do
+ it 'only returns confidential_only issues' do
+ create(:issue)
+ confidential_issue = create(:issue, confidential: true)
+
+ expect(described_class.confidential_only).to eq([confidential_issue])
+ end
+ end
+
it_behaves_like 'throttled touch' do
subject { create(:issue, updated_at: 1.hour.ago) }
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 1849d3bac12..e530e0302f5 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -3,6 +3,18 @@ require 'spec_helper'
describe MergeRequestDiff do
let(:diff_with_commits) { create(:merge_request).merge_request_diff }
+ describe 'validations' do
+ subject { diff_with_commits }
+
+ it 'checks sha format of base_commit_sha, head_commit_sha and start_commit_sha' do
+ subject.base_commit_sha = subject.head_commit_sha = subject.start_commit_sha = 'foobar'
+
+ expect(subject.valid?).to be false
+ expect(subject.errors.count).to eq 3
+ expect(subject.errors).to all(include('is not a valid SHA'))
+ end
+ end
+
describe 'create new record' do
subject { diff_with_commits }
@@ -78,7 +90,7 @@ describe MergeRequestDiff do
it 'returns persisted diffs if cannot compare with diff refs' do
expect(diff).to receive(:load_diffs).and_call_original
- diff.update!(head_commit_sha: 'invalid-sha')
+ diff.update!(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex))
diff.diffs.diff_files
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index afa87b8a62d..82a853a23b9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -435,6 +435,7 @@ describe MergeRequest do
it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
@@ -1023,23 +1024,23 @@ describe MergeRequest do
end
end
- describe '#committers' do
- it 'returns all the committers of every commit in the merge request' do
- users = subject.commits.map(&:committer_email).uniq.map do |email|
+ describe '#commit_authors' do
+ it 'returns all the authors of every commit in the merge request' do
+ users = subject.commits.map(&:author_email).uniq.map do |email|
create(:user, email: email)
end
- expect(subject.committers).to match_array(users)
+ expect(subject.commit_authors).to match_array(users)
end
- it 'returns an empty array if no committer is associated with a user' do
- expect(subject.committers).to be_empty
+ it 'returns an empty array if no author is associated with a user' do
+ expect(subject.commit_authors).to be_empty
end
end
describe '#authors' do
- it 'returns a list with all the committers in the merge request and author' do
- users = subject.commits.map(&:committer_email).uniq.map do |email|
+ it 'returns a list with all the commit authors in the merge request and author' do
+ users = subject.commits.map(&:author_email).uniq.map do |email|
create(:user, email: email)
end
@@ -2604,8 +2605,9 @@ describe MergeRequest do
let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) }
+ let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) }
- it 'returns latest pipeline' do
+ it 'returns latest pipeline for the target branch' do
expect(merge_request.base_pipeline).to eq(last_pipeline)
end
end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index b6cf4c72450..e9c7c94ad70 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -33,18 +33,38 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
describe 'Validations' do
context 'when manual_configuration is enabled' do
before do
- subject.manual_configuration = true
+ service.manual_configuration = true
end
- it { is_expected.to validate_presence_of(:api_url) }
+ it 'validates presence of api_url' do
+ expect(service).to validate_presence_of(:api_url)
+ end
end
context 'when manual configuration is disabled' do
before do
- subject.manual_configuration = false
+ service.manual_configuration = false
end
- it { is_expected.not_to validate_presence_of(:api_url) }
+ it 'does not validate presence of api_url' do
+ expect(service).not_to validate_presence_of(:api_url)
+ end
+ end
+
+ context 'when the api_url domain points to localhost or local network' do
+ let(:domain) { Addressable::URI.parse(service.api_url).hostname }
+
+ it 'cannot query' do
+ expect(service.can_query?).to be true
+
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be false
+ end
+ end
+ end
end
end
@@ -74,30 +94,35 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
describe '#prometheus_client' do
+ let(:api_url) { 'http://some_url' }
+
+ before do
+ service.active = true
+ service.api_url = api_url
+ service.manual_configuration = manual_configuration
+ end
+
context 'manual configuration is enabled' do
- let(:api_url) { 'http://some_url' }
+ let(:manual_configuration) { true }
- before do
- subject.active = true
- subject.manual_configuration = true
- subject.api_url = api_url
+ it 'returns rest client from api_url' do
+ expect(service.prometheus_client.url).to eq(api_url)
end
- it 'returns rest client from api_url' do
- expect(subject.prometheus_client.url).to eq(api_url)
+ it 'calls valid?' do
+ allow(service).to receive(:valid?).and_call_original
+
+ expect(service.prometheus_client).not_to be_nil
+
+ expect(service).to have_received(:valid?)
end
end
context 'manual configuration is disabled' do
- let(:api_url) { 'http://some_url' }
-
- before do
- subject.manual_configuration = false
- subject.api_url = api_url
- end
+ let(:manual_configuration) { false }
it 'no client provided' do
- expect(subject.prometheus_client).to be_nil
+ expect(service.prometheus_client).to be_nil
end
end
end
diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb
index 0d95f454819..5c4bce90ace 100644
--- a/spec/models/project_services/slack_slash_commands_service_spec.rb
+++ b/spec/models/project_services/slack_slash_commands_service_spec.rb
@@ -38,4 +38,11 @@ describe SlackSlashCommandsService do
end
end
end
+
+ describe '#chat_responder' do
+ it 'returns the responder to use for Slack' do
+ expect(described_class.new.chat_responder)
+ .to eq(Gitlab::Chat::Responder::Slack)
+ end
+ end
end
diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb
new file mode 100644
index 00000000000..9524b526a46
--- /dev/null
+++ b/spec/models/project_services/youtrack_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe YoutrackService do
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:project_url) }
+ it { is_expected.to validate_presence_of(:issues_url) }
+ it_behaves_like 'issue tracker service URL attribute', :project_url
+ it_behaves_like 'issue tracker service URL attribute', :issues_url
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:project_url) }
+ it { is_expected.not_to validate_presence_of(:issues_url) }
+ end
+ end
+
+ describe '.reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does allow project prefix on the reference' do
+ expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123')
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1f9088c2e6b..9cc9894003d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -50,6 +50,7 @@ describe Project do
it { is_expected.to have_one(:teamcity_service) }
it { is_expected.to have_one(:jira_service) }
it { is_expected.to have_one(:redmine_service) }
+ it { is_expected.to have_one(:youtrack_service) }
it { is_expected.to have_one(:custom_issue_tracker_service) }
it { is_expected.to have_one(:bugzilla_service) }
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
@@ -428,6 +429,30 @@ describe Project do
end
end
+ describe '#ci_pipelines' do
+ let(:project) { create(:project) }
+
+ before do
+ create(:ci_pipeline, project: project, ref: 'master', source: :web)
+ create(:ci_pipeline, project: project, ref: 'master', source: :external)
+ end
+
+ it 'has ci pipelines' do
+ expect(project.ci_pipelines.size).to eq(2)
+ end
+
+ context 'when builds are disabled' do
+ before do
+ project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
+ end
+
+ it 'should return .external pipelines' do
+ expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
+ expect(project.ci_pipelines.size).to eq(1)
+ end
+ end
+ end
+
describe 'project token' do
it 'sets an random token if none provided' do
project = FactoryBot.create(:project, runners_token: '')
@@ -458,6 +483,7 @@ describe Project do
it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
end
describe '#to_reference_with_postfix' do
@@ -2486,6 +2512,16 @@ describe Project do
end
end
+ describe '#set_repository_writable!' do
+ it 'sets repository_read_only to false' do
+ project = create(:project, :read_only)
+
+ expect { project.set_repository_writable! }
+ .to change(project, :repository_read_only)
+ .from(true).to(false)
+ end
+ end
+
describe '#pushes_since_gc' do
let(:project) { create(:project) }
diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb
index 2b978c1c8ff..3610408c138 100644
--- a/spec/models/prometheus_metric_spec.rb
+++ b/spec/models/prometheus_metric_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
describe PrometheusMetric do
subject { build(:prometheus_metric) }
- let(:other_project) { build(:project) }
it_behaves_like 'having unique enum values'
@@ -16,17 +15,17 @@ describe PrometheusMetric do
describe 'common metrics' do
using RSpec::Parameterized::TableSyntax
- where(:common, :project, :result) do
- false | other_project | true
- false | nil | false
- true | other_project | false
- true | nil | true
+ where(:common, :with_project, :result) do
+ false | true | true
+ false | false | false
+ true | true | false
+ true | false | true
end
with_them do
before do
subject.common = common
- subject.project = project
+ subject.project = with_project ? build(:project) : nil
end
it { expect(subject.valid?).to eq(result) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f78760bf567..17201d8b90a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1373,6 +1373,29 @@ describe Repository do
end
end
+ describe '#merge_to_ref' do
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature',
+ target_branch: 'master',
+ source_project: project)
+ end
+
+ it 'writes merge of source and target to MR merge_ref_path' do
+ merge_commit_id = repository.merge_to_ref(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ merge_request.merge_ref_path,
+ 'Custom message')
+
+ merge_commit = repository.commit(merge_commit_id)
+
+ expect(merge_commit.message).to eq('Custom message')
+ expect(merge_commit.author_name).to eq(user.name)
+ expect(merge_commit.author_email).to eq(user.commit_email)
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
+ end
+
describe '#ff_merge' do
before do
repository.add_branch(user, 'ff-target', 'feature~5')
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 78477ab0a5a..85b157a9435 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -925,6 +925,21 @@ describe User do
expect(user.manageable_groups).to contain_exactly(group, subgroup)
end
end
+
+ describe '#manageable_groups_with_routes' do
+ it 'eager loads routes from manageable groups' do
+ control_count =
+ ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ user.manageable_groups_with_routes.map(&:route)
+ end.count
+
+ create(:group, parent: subgroup)
+
+ expect do
+ user.manageable_groups_with_routes.map(&:route)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+ end
end
end
@@ -961,43 +976,43 @@ describe User do
end
end
- describe '.filter' do
+ describe '.filter_items' do
let(:user) { double }
it 'filters by active users by default' do
expect(described_class).to receive(:active).and_return([user])
- expect(described_class.filter(nil)).to include user
+ expect(described_class.filter_items(nil)).to include user
end
it 'filters by admins' do
expect(described_class).to receive(:admins).and_return([user])
- expect(described_class.filter('admins')).to include user
+ expect(described_class.filter_items('admins')).to include user
end
it 'filters by blocked' do
expect(described_class).to receive(:blocked).and_return([user])
- expect(described_class.filter('blocked')).to include user
+ expect(described_class.filter_items('blocked')).to include user
end
it 'filters by two_factor_disabled' do
expect(described_class).to receive(:without_two_factor).and_return([user])
- expect(described_class.filter('two_factor_disabled')).to include user
+ expect(described_class.filter_items('two_factor_disabled')).to include user
end
it 'filters by two_factor_enabled' do
expect(described_class).to receive(:with_two_factor).and_return([user])
- expect(described_class.filter('two_factor_enabled')).to include user
+ expect(described_class.filter_items('two_factor_enabled')).to include user
end
it 'filters by wop' do
expect(described_class).to receive(:without_projects).and_return([user])
- expect(described_class.filter('wop')).to include user
+ expect(described_class.filter_items('wop')).to include user
end
end
diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb
new file mode 100644
index 00000000000..4b76d65ef69
--- /dev/null
+++ b/spec/policies/board_policy_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BoardPolicy do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :private) }
+ let(:group) { create(:group, :private) }
+ let(:group_board) { create(:board, group: group) }
+ let(:project_board) { create(:board, project: project) }
+
+ let(:board_permissions) do
+ [
+ :read_parent,
+ :read_milestone,
+ :read_issue
+ ]
+ end
+
+ def expect_allowed(*permissions)
+ permissions.each { |p| is_expected.to be_allowed(p) }
+ end
+
+ def expect_disallowed(*permissions)
+ permissions.each { |p| is_expected.not_to be_allowed(p) }
+ end
+
+ context 'group board' do
+ subject { described_class.new(user, group_board) }
+
+ context 'user has access' do
+ before do
+ group.add_developer(user)
+ end
+
+ it do
+ expect_allowed(*board_permissions)
+ end
+ end
+
+ context 'user does not have access' do
+ it do
+ expect_disallowed(*board_permissions)
+ end
+ end
+ end
+
+ context 'project board' do
+ subject { described_class.new(user, project_board) }
+
+ context 'user has access' do
+ before do
+ project.add_developer(user)
+ end
+
+ it do
+ expect_allowed(*board_permissions)
+ end
+ end
+
+ context 'user does not have access' do
+ it do
+ expect_disallowed(*board_permissions)
+ end
+ end
+ end
+end
diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb
new file mode 100644
index 00000000000..2259693cf01
--- /dev/null
+++ b/spec/policies/commit_policy_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe CommitPolicy do
+ describe '#rules' do
+ let(:user) { create(:user) }
+ let(:commit) { project.repository.head_commit }
+ let(:policy) { described_class.new(user, commit) }
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+
+ context 'when repository access level is private' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'can not read commit and create a note' do
+ expect(policy).to be_disallowed(:read_commit)
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it 'can not read commit and create a note' do
+ expect(policy).to be_disallowed(:read_commit)
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index be1804c5ce0..4c31ff30fc6 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -74,6 +74,38 @@ describe GroupPolicy do
end
end
+ context 'with no user and public project' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:current_user) { nil }
+
+ before do
+ Projects::GroupLinks::CreateService.new(
+ project,
+ user,
+ link_group_access: ProjectGroupLink::DEVELOPER
+ ).execute(group)
+ end
+
+ it { expect_disallowed(:read_group) }
+ end
+
+ context 'with foreign user and public project' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ before do
+ Projects::GroupLinks::CreateService.new(
+ project,
+ user,
+ link_group_access: ProjectGroupLink::DEVELOPER
+ ).execute(group)
+ end
+
+ it { expect_disallowed(:read_group) }
+ end
+
context 'has projects' do
let(:current_user) { create(:user) }
let(:project) { create(:project, namespace: group) }
@@ -82,17 +114,13 @@ describe GroupPolicy do
project.add_developer(current_user)
end
- it do
- expect_allowed(:read_group, :read_label)
- end
+ it { expect_allowed(:read_label) }
context 'in subgroups', :nested_groups do
let(:subgroup) { create(:group, :private, parent: group) }
let(:project) { create(:project, namespace: subgroup) }
- it do
- expect_allowed(:read_group, :read_label)
- end
+ it { expect_allowed(:read_label) }
end
end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index 0e848c74659..4be7a0266d1 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -1,28 +1,15 @@
require 'spec_helper'
-describe NotePolicy, mdoels: true do
+describe NotePolicy do
describe '#rules' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
-
- def policies(noteable = nil)
- return @policies if @policies
-
- noteable ||= issue
- note = if noteable.is_a?(Commit)
- create(:note_on_commit, commit_id: noteable.id, author: user, project: project)
- else
- create(:note, noteable: noteable, author: user, project: project)
- end
-
- @policies = described_class.new(user, note)
- end
+ let(:noteable) { issue }
+ let(:policy) { described_class.new(user, note) }
+ let(:note) { create(:note, noteable: noteable, author: user, project: project) }
shared_examples_for 'a discussion with a private noteable' do
- let(:noteable) { issue }
- let(:policy) { policies(noteable) }
-
context 'when the note author can no longer see the noteable' do
it 'can not edit nor read the note' do
expect(policy).to be_disallowed(:admin_note)
@@ -46,12 +33,21 @@ describe NotePolicy, mdoels: true do
end
end
- context 'when the project is private' do
- let(:project) { create(:project, :private, :repository) }
+ context 'when the noteable is a commit' do
+ let(:commit) { project.repository.head_commit }
+ let(:note) { create(:note_on_commit, commit_id: commit.id, author: user, project: project) }
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it_behaves_like 'a discussion with a private noteable'
+ end
- context 'when the noteable is a commit' do
- it_behaves_like 'a discussion with a private noteable' do
- let(:noteable) { project.repository.head_commit }
+ context 'when the project is public' do
+ context 'when repository access level is private' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it_behaves_like 'a discussion with a private noteable'
end
end
end
@@ -59,44 +55,44 @@ describe NotePolicy, mdoels: true do
context 'when the project is public' do
context 'when the note author is not a project member' do
it 'can edit a note' do
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
end
context 'when the noteable is a project snippet' do
- it 'can edit note' do
- policies = policies(create(:project_snippet, :public, project: project))
+ let(:noteable) { create(:project_snippet, :public, project: project) }
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ it 'can edit note' do
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
context 'when it is private' do
- it_behaves_like 'a discussion with a private noteable' do
- let(:noteable) { create(:project_snippet, :private, project: project) }
- end
+ let(:noteable) { create(:project_snippet, :private, project: project) }
+
+ it_behaves_like 'a discussion with a private noteable'
end
end
context 'when the noteable is a personal snippet' do
- it 'can edit note' do
- policies = policies(create(:personal_snippet, :public))
+ let(:noteable) { create(:personal_snippet, :public) }
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ it 'can edit note' do
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
context 'when it is private' do
- it 'can not edit nor read the note' do
- policies = policies(create(:personal_snippet, :private))
+ let(:noteable) { create(:personal_snippet, :private) }
- expect(policies).to be_disallowed(:admin_note)
- expect(policies).to be_disallowed(:resolve_note)
- expect(policies).to be_disallowed(:read_note)
+ it 'can not edit nor read the note' do
+ expect(policy).to be_disallowed(:admin_note)
+ expect(policy).to be_disallowed(:resolve_note)
+ expect(policy).to be_disallowed(:read_note)
end
end
end
@@ -120,20 +116,20 @@ describe NotePolicy, mdoels: true do
end
it 'can edit a note' do
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
end
context 'when the note author is not a project member' do
it 'can not edit a note' do
- expect(policies).to be_disallowed(:admin_note)
- expect(policies).to be_disallowed(:resolve_note)
+ expect(policy).to be_disallowed(:admin_note)
+ expect(policy).to be_disallowed(:resolve_note)
end
it 'can read a note' do
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:read_note)
end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 93a468f585b..f8d581ef38f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -130,22 +130,26 @@ describe ProjectPolicy do
subject { described_class.new(owner, project) }
context 'when the feature is disabled' do
- it 'does not include the issues permissions' do
+ before do
project.issues_enabled = false
project.save!
+ end
+ it 'does not include the issues permissions' do
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
end
- end
- context 'when the feature is disabled and external tracker configured' do
- it 'does not include the issues permissions' do
- create(:jira_service, project: project)
+ it 'disables boards and lists permissions' do
+ expect_disallowed :read_board, :create_board, :update_board, :admin_board
+ expect_disallowed :read_list, :create_list, :update_list, :admin_list
+ end
- project.issues_enabled = false
- project.save!
+ context 'when external tracker configured' do
+ it 'does not include the issues permissions' do
+ create(:jira_service, project: project)
- expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
+ expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
+ end
end
end
end
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index 170e0ac5717..f50bcf54b46 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -98,4 +98,72 @@ describe Ci::BuildRunnerPresenter do
end
end
end
+
+ describe '#ref_type' do
+ subject { presenter.ref_type }
+
+ let(:build) { create(:ci_build, tag: tag) }
+ let(:tag) { true }
+
+ it 'returns the correct ref type' do
+ is_expected.to eq('tag')
+ end
+
+ context 'when tag is false' do
+ let(:tag) { false }
+
+ it 'returns the correct ref type' do
+ is_expected.to eq('branch')
+ end
+ end
+ end
+
+ describe '#git_depth' do
+ subject { presenter.git_depth }
+
+ let(:build) { create(:ci_build) }
+
+ it 'returns the correct git depth' do
+ is_expected.to eq(0)
+ end
+
+ context 'when GIT_DEPTH variable is specified' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
+ end
+
+ it 'returns the correct git depth' do
+ is_expected.to eq(1)
+ end
+ end
+ end
+
+ describe '#refspecs' do
+ subject { presenter.refspecs }
+
+ let(:build) { create(:ci_build) }
+
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*')
+ end
+
+ context 'when GIT_DEPTH variable is specified' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
+ end
+
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ end
+
+ context 'when ref is tag' do
+ let(:build) { create(:ci_build, :tag) }
+
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index bc892523cf8..a132b85b878 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1430,8 +1430,8 @@ describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
- let!(:project) { create(:project, :repository, :private) }
- let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let(:project) { create(:project, :repository, :private) }
+ let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
it 'returns the correct merge request' do
@@ -1456,5 +1456,54 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(404)
end
+
+ context 'public project' do
+ let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let(:non_member) { create(:user) }
+
+ it 'responds 403 when only members are allowed to read merge requests' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", non_member)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/signature' do
+ let!(:project) { create(:project, :repository, :public) }
+ let(:project_id) { project.id }
+ let(:commit_id) { project.repository.commit.id }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" }
+
+ context 'when commit does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'unsigned commit' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 GPG Signature Not Found'}
+ end
+ end
+
+ context 'signed commit' do
+ let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) }
+ let(:commit_id) { commit.id }
+
+ it 'returns correct JSON' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['gpg_key_id']).to eq(commit.signature.gpg_key_id)
+ expect(json_response['gpg_key_subkey_id']).to eq(commit.signature.gpg_key_subkey_id)
+ expect(json_response['gpg_key_primary_keyid']).to eq(commit.signature.gpg_key_primary_keyid)
+ expect(json_response['verification_status']).to eq(commit.signature.verification_status)
+ end
+ end
end
end
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 22a9e36ca31..57a57e69a00 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -163,6 +163,40 @@ describe API::Features do
end
end
+ context 'when enabling for a group by path' do
+ context 'when the group exists' do
+ it 'sets the feature gate' do
+ group = create(:group)
+
+ post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to eq(
+ 'name' => 'my_feature',
+ 'state' => 'conditional',
+ 'gates' => [
+ { 'key' => 'boolean', 'value' => false },
+ { 'key' => 'actors', 'value' => ["Group:#{group.id}"] }
+ ])
+ end
+ end
+
+ context 'when the group does not exist' do
+ it 'sets no new values and keeps the feature disabled' do
+ post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response).to eq(
+ "name" => "my_feature",
+ "state" => "off",
+ "gates" => [
+ { "key" => "boolean", "value" => false }
+ ]
+ )
+ end
+ end
+ end
+
it 'creates a feature with the given percentage if passed an integer' do
post api("/features/#{feature_name}", admin), params: { value: '50' }
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 355336ad7e2..c2934430821 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -56,4 +56,38 @@ describe 'getting an issue list for a project' do
expect(issues_data).to eq []
end
end
+
+ context 'when there is a confidential issue' do
+ let!(:confidential_issue) do
+ create(:issue, :confidential, project: project)
+ end
+
+ context 'when the user cannot see confidential issues' do
+ it 'returns issues without confidential issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_data.size).to eq(2)
+
+ issues_data.each do |issue|
+ expect(issue.dig('node', 'confidential')).to eq(false)
+ end
+ end
+ end
+
+ context 'when the user can see confidential issues' do
+ it 'returns issues with confidential issues' do
+ project.add_developer(current_user)
+
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_data.size).to eq(3)
+
+ confidentials = issues_data.map do |issue|
+ issue.dig('node', 'confidential')
+ end
+
+ expect(confidentials).to eq([true, false, false])
+ end
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 6a943b5237a..cd85151ec1b 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -167,6 +167,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME)
expect(Gitlab::LfsToken.new(key).token_valid?(json_response['lfs_token'])).to be_truthy
end
@@ -324,6 +325,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq('/')
+ expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to be_nil
end
@@ -336,6 +338,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq('/')
+ expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
end
@@ -349,6 +352,7 @@ describe API::Internal do
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
+ expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
@@ -368,6 +372,7 @@ describe API::Internal do
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
+ expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 46cd3ec88e1..f35dabf5d0f 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -183,6 +183,18 @@ describe API::Issues do
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
+ it 'returns only confidential issues' do
+ get api('/issues', user), params: { confidential: true, scope: 'all' }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api('/issues', user), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
it 'returns issues reacted by the authenticated user' do
issue2 = create(:issue, project: project, author: user, assignees: [user])
create(:award_emoji, awardable: issue2, user: user2, name: 'star')
@@ -354,15 +366,43 @@ describe API::Issues do
end
it 'returns an empty array if iid does not exist' do
- get api("/issues", user), params: { iids: [99999] }
+ get api("/issues", user), params: { iids: [0] }
expect_paginated_array_response([])
end
- it 'sorts by created_at descending by default' do
- get api('/issues', user)
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api('/issues', user)
- expect_paginated_array_response([issue.id, closed_issue.id])
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api('/issues?per_page=2', user)
+
+ expect_paginated_array_response([issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api('/issues?per_page=2&page=2', user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
end
it 'sorts ascending when requested' do
@@ -393,6 +433,24 @@ describe API::Issues do
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
+
+ it 'returns a related merge request count of 0 if there are no related merge requests' do
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 0)
+ end
+
+ it 'returns a related merge request count > 0 if there are related merge requests' do
+ create(:merge_requests_closing_issues, issue: issue)
+
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 1)
+ end
end
end
@@ -511,6 +569,18 @@ describe API::Issues do
expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
end
+ it 'returns only confidential issues' do
+ get api(base_url, user), params: { confidential: true }
+
+ expect_paginated_array_response(group_confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api(base_url, user), params: { confidential: false }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
it 'returns an array of labeled group issues' do
get api(base_url, user), params: { labels: group_label.title }
@@ -557,7 +627,7 @@ describe API::Issues do
end
it 'returns an empty array if iid does not exist' do
- get api(base_url, user), params: { iids: [99999] }
+ get api(base_url, user), params: { iids: [0] }
expect_paginated_array_response([])
end
@@ -613,10 +683,38 @@ describe API::Issues do
expect_paginated_array_response(group_confidential_issue.id)
end
- it 'sorts by created_at descending by default' do
- get api(base_url, user)
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:group_issue2) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description,
+ created_at: group_issue.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}?per_page=3", user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}?per_page=3&page=2", user)
+
+ expect_paginated_array_response([group_issue.id])
+ end
+ end
end
it 'sorts ascending when requested' do
@@ -708,6 +806,18 @@ describe API::Issues do
expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
end
+ it 'returns only confidential issues' do
+ get api("#{base_url}/issues", author), params: { confidential: true }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api("#{base_url}/issues", author), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
@@ -763,7 +873,7 @@ describe API::Issues do
end
it 'returns an empty array if iid does not exist' do
- get api("#{base_url}/issues", user), params: { iids: [99999] }
+ get api("#{base_url}/issues", user), params: { iids: [0] }
expect_paginated_array_response([])
end
@@ -828,10 +938,38 @@ describe API::Issues do
expect_paginated_array_response([issue.id, closed_issue.id])
end
- it 'sorts by created_at descending by default' do
- get api("#{base_url}/issues", user)
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api("#{base_url}/issues", user)
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}/issues?per_page=3", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}/issues?per_page=3&page=2", user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
end
it 'sorts ascending when requested' do
@@ -1771,7 +1909,7 @@ describe API::Issues do
end
it "returns 404 when issue doesn't exists" do
- get api("/projects/#{project.id}/issues/9999/closed_by", user)
+ get api("/projects/#{project.id}/issues/0/closed_by", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -1792,7 +1930,7 @@ describe API::Issues do
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
- create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
end
end
@@ -1829,6 +1967,24 @@ describe API::Issues do
expect_paginated_array_response(related_mr.id)
end
+ it 'returns merge requests cross-project wide' do
+ project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ merge_request = create_referencing_mr(user, project2, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response([related_mr.id, merge_request.id])
+ end
+
+ it 'does not generate references to projects with no access' do
+ private_project = create(:project, :private)
+ create_referencing_mr(private_project.creator, private_project, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
context 'no merge request mentioned a issue' do
it 'returns empty array' do
get_related_merge_requests(project.id, closed_issue.iid, user)
@@ -1838,7 +1994,7 @@ describe API::Issues do
end
it "returns 404 when issue doesn't exists" do
- get_related_merge_requests(project.id, 999999, user)
+ get_related_merge_requests(project.id, 0, user)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 3c4719964b6..f37d84fddef 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -16,7 +16,7 @@ describe API::Keys do
context 'when authenticated' do
it 'returns 404 for non-existing key' do
- get api('/keys/999999', admin)
+ get api('/keys/0', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 606fa9185d8..518181e4d93 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -22,7 +22,7 @@ describe API::Labels do
expected_keys = %w(
id name color text_color description
open_issues_count closed_issues_count open_merge_requests_count
- subscribed priority
+ subscribed priority is_project_label
)
get api("/projects/#{project.id}/labels", user)
@@ -47,6 +47,7 @@ describe API::Labels do
expect(label1_response['description']).to be_nil
expect(label1_response['priority']).to be_nil
expect(label1_response['subscribed']).to be_falsey
+ expect(label1_response['is_project_label']).to be_truthy
expect(group_label_response['open_issues_count']).to eq(1)
expect(group_label_response['closed_issues_count']).to eq(0)
@@ -57,6 +58,7 @@ describe API::Labels do
expect(group_label_response['description']).to be_nil
expect(group_label_response['priority']).to be_nil
expect(group_label_response['subscribed']).to be_falsey
+ expect(group_label_response['is_project_label']).to be_falsey
expect(priority_label_response['open_issues_count']).to eq(0)
expect(priority_label_response['closed_issues_count']).to eq(0)
@@ -67,6 +69,7 @@ describe API::Labels do
expect(priority_label_response['description']).to be_nil
expect(priority_label_response['priority']).to eq(3)
expect(priority_label_response['subscribed']).to be_falsey
+ expect(priority_label_response['is_project_label']).to be_truthy
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 6530dc956cb..8a67d98fc4c 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -30,7 +30,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ get api("/projects/#{project.id}/merge_requests/0/versions", user)
expect(response).to have_gitlab_http_status(404)
end
end
@@ -53,7 +53,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
end
it 'returns a 404 when merge_request version_id is not found' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0f5f6e38819..db56739af2f 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -372,6 +372,7 @@ describe API::MergeRequests do
expect(json_response['force_close_merge_request']).to be_falsy
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
expect(json_response['merge_error']).to eq(merge_request.merge_error)
+ expect(json_response['user']['can_merge']).to be_truthy
expect(json_response).not_to include('rebase_in_progress')
end
@@ -440,7 +441,7 @@ describe API::MergeRequests do
end
it "returns a 404 error if merge_request_iid not found" do
- get api("/projects/#{project.id}/merge_requests/999", user)
+ get api("/projects/#{project.id}/merge_requests/0", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -499,6 +500,15 @@ describe API::MergeRequests do
expect(json_response['allow_maintainer_to_push']).to be_truthy
end
end
+
+ it 'indicates if a user cannot merge the MR' do
+ user2 = create(:user)
+ project.add_reporter(user2)
+
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user2)
+
+ expect(json_response['user']['can_merge']).to be_falsy
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do
@@ -521,7 +531,7 @@ describe API::MergeRequests do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/commits", user)
+ get api("/projects/#{project.id}/merge_requests/0/commits", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -541,7 +551,7 @@ describe API::MergeRequests do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/changes", user)
+ get api("/projects/#{project.id}/merge_requests/0/changes", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -974,6 +984,21 @@ describe API::MergeRequests do
expect(squash_commit.message).to eq(merge_request.default_squash_commit_message)
end
end
+
+ describe "the should_remove_source_branch param" do
+ let(:source_repository) { merge_request.source_project.repository }
+ let(:source_branch) { merge_request.source_branch }
+
+ it 'removes the source branch when set' do
+ put(
+ api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user),
+ params: { should_remove_source_branch: true }
+ )
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(source_repository.branch_exists?(source_branch)).to be_falsy
+ end
+ end
end
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 145356c4df5..2e376109b42 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -149,7 +149,7 @@ describe API::Namespaces do
context "when namespace doesn't exist" do
it 'returns not-found' do
- get api('/namespaces/9999', request_actor)
+ get api('/namespaces/0', request_actor)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 49b5dfb0b33..895f05a98e8 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -23,13 +23,13 @@ describe API::ProjectMilestones do
end
it 'returns 404 response when the project does not exists' do
- delete api("/projects/999/milestones/#{milestone.id}", user)
+ delete api("/projects/0/milestones/#{milestone.id}", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 response when the milestone does not exists' do
- delete api("/projects/#{project.id}/milestones/999", user)
+ delete api("/projects/#{project.id}/milestones/0", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -49,4 +49,74 @@ describe API::ProjectMilestones do
params: { state_event: 'close' }
end
end
+
+ describe 'POST /projects/:id/milestones/:milestone_id/promote' do
+ let(:group) { create(:group) }
+
+ before do
+ project.update(namespace: group)
+ end
+
+ context 'when user does not have permission to promote milestone' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'returns 403' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when user has permission' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns 200' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(group.milestones.first.title).to eq(milestone.title)
+ end
+
+ it 'returns 200 for closed milestone' do
+ post api("/projects/#{project.id}/milestones/#{closed_milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(group.milestones.first.title).to eq(closed_milestone.title)
+ end
+ end
+
+ context 'when no such resources' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns 404 response when the project does not exist' do
+ post api("/projects/0/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 response when the milestone does not exist' do
+ post api("/projects/#{project.id}/milestones/0/promote", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when project does not belong to group' do
+ before do
+ project.update(namespace: user.namespace)
+ end
+
+ it 'returns 403' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/project_templates_spec.rb b/spec/requests/api/project_templates_spec.rb
index ab5d4de7ff7..80e5033dab4 100644
--- a/spec/requests/api/project_templates_spec.rb
+++ b/spec/requests/api/project_templates_spec.rb
@@ -92,6 +92,22 @@ describe API::ProjectTemplates do
expect(json_response['name']).to eq('Actionscript')
end
+ it 'returns C++ gitignore' do
+ get api("/projects/#{public_project.id}/templates/gitignores/C++")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/template')
+ expect(json_response['name']).to eq('C++')
+ end
+
+ it 'returns C++ gitignore for URL-encoded names' do
+ get api("/projects/#{public_project.id}/templates/gitignores/C%2B%2B")
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/template')
+ expect(json_response['name']).to eq('C++')
+ end
+
it 'returns a specific gitlab_ci_yml' do
get api("/projects/#{public_project.id}/templates/gitlab_ci_ymls/Android")
@@ -125,6 +141,18 @@ describe API::ProjectTemplates do
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/license')
end
+
+ shared_examples 'path traversal attempt' do |template_type|
+ it 'rejects invalid filenames' do
+ get api("/projects/#{public_project.id}/templates/#{template_type}/%2e%2e%2fPython%2ea")
+
+ expect(response).to have_gitlab_http_status(500)
+ end
+ end
+
+ TemplateFinder::VENDORED_TEMPLATES.each do |template_type, _|
+ it_behaves_like 'path traversal attempt', template_type
+ end
end
describe 'GET /projects/:id/templates/licenses/:key' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index cfa7a1a31a3..60d9d7fed13 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -4,6 +4,15 @@ require 'spec_helper'
shared_examples 'languages and percentages JSON response' do
let(:expected_languages) { project.repository.languages.map { |language| language.values_at(:label, :value)}.to_h }
+ before do
+ allow(project.repository).to receive(:languages).and_return(
+ [{ value: 66.69, label: "Ruby", color: "#701516", highlight: "#701516" },
+ { value: 22.98, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
+ { value: 7.91, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
+ { value: 2.42, label: "CoffeeScript", color: "#244776", highlight: "#244776" }]
+ )
+ end
+
it 'returns expected language values' do
get api("/projects/#{project.id}/languages", user)
@@ -11,6 +20,23 @@ shared_examples 'languages and percentages JSON response' do
expect(json_response).to eq(expected_languages)
expect(json_response.count).to be > 1
end
+
+ context 'when the languages were detected before' do
+ before do
+ Projects::DetectRepositoryLanguagesService.new(project, project.owner).execute
+ end
+
+ it 'returns the detection from the database' do
+ # Allow this to happen once, so the expected languages can be determined
+ expect(project.repository).to receive(:languages).once
+
+ get api("/projects/#{project.id}/languages", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(expected_languages)
+ expect(json_response.count).to be > 1
+ end
+ end
end
describe API::Projects do
@@ -110,6 +136,7 @@ describe API::Projects do
end
let!(:public_project) { create(:project, :public, name: 'public_project') }
+
before do
project
project2
@@ -752,7 +779,7 @@ describe API::Projects do
let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) }
it 'returns error when user not found' do
- get api('/users/9999/projects/')
+ get api('/users/0/projects/')
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -942,8 +969,16 @@ describe API::Projects do
describe 'GET /projects/:id' do
context 'when unauthenticated' do
- it 'returns the public projects' do
- public_project = create(:project, :public)
+ it 'does not return private projects' do
+ private_project = create(:project, :private)
+
+ get api("/projects/#{private_project.id}")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns public projects' do
+ public_project = create(:project, :repository, :public)
get api("/projects/#{public_project.id}")
@@ -951,8 +986,34 @@ describe API::Projects do
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
expect(json_response['default_branch']).to eq(public_project.default_branch)
+ expect(json_response['ci_config_path']).to eq(public_project.ci_config_path)
expect(json_response.keys).not_to include('permissions')
end
+
+ context 'and the project has a private repository' do
+ let(:project) { create(:project, :repository, :public, :repository_private) }
+ let(:protected_attributes) { %w(default_branch ci_config_path) }
+
+ it 'hides protected attributes of private repositories if user is not a member' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ protected_attributes.each do |attribute|
+ expect(json_response.keys).not_to include(attribute)
+ end
+ end
+
+ it 'exposes protected attributes of private repositories if user is a member' do
+ project.add_developer(user)
+
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ protected_attributes.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+ end
end
context 'when authenticated' do
@@ -1104,6 +1165,26 @@ describe API::Projects do
expect(json_response).to include 'statistics'
end
+ context "and the project has a private repository" do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it "does not include statistics if user is not a member" do
+ get api("/projects/#{project.id}", user), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).not_to include 'statistics'
+ end
+
+ it "includes statistics if user is a member" do
+ project.add_developer(user)
+
+ get api("/projects/#{project.id}", user), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to include 'statistics'
+ end
+ end
+
it "includes import_error if user can admin project" do
get api("/projects/#{project.id}", user)
@@ -1359,7 +1440,7 @@ describe API::Projects do
end
it 'fails if forked_from project which does not exist' do
- post api("/projects/#{project_fork_target.id}/fork/9999", admin)
+ post api("/projects/#{project_fork_target.id}/fork/0", admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -1484,6 +1565,9 @@ describe API::Projects do
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
+ before do
+ group.add_developer(user)
+ end
it "shares project with group" do
expires_at = 10.days.from_now.to_date
@@ -1534,6 +1618,15 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
+
+ it "returns a 409 error when link is not saved" do
+ allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
+ .and_return({ status: :error, http_status: 409, message: 'error' })
+
+ post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(409)
+ end
end
describe 'DELETE /projects/:id/share/:group_id' do
@@ -1910,7 +2003,7 @@ describe API::Projects do
end
it 'returns not_found(404) for not existing project' do
- get api("/projects/9999999999/languages", user)
+ get api("/projects/0/languages", user)
expect(response).to have_gitlab_http_status(:not_found)
end
@@ -1995,6 +2088,11 @@ describe API::Projects do
let(:project) do
create(:project, :repository, creator: user, namespace: user.namespace)
end
+
+ let(:project2) do
+ create(:project, :repository, creator: user, namespace: user.namespace)
+ end
+
let(:group) { create(:group) }
let(:group2) do
group = create(:group, name: 'group2_name')
@@ -2010,6 +2108,7 @@ describe API::Projects do
before do
project.add_reporter(user2)
+ project2.add_reporter(user2)
end
context 'when authenticated' do
@@ -2124,6 +2223,48 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(201)
expect(json_response['namespace']['name']).to eq(group.name)
end
+
+ it 'accepts a path for the target project' do
+ post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to eq('foobar')
+ expect(json_response['owner']['id']).to eq(user2.id)
+ expect(json_response['namespace']['id']).to eq(user2.namespace.id)
+ expect(json_response['forked_from_project']['id']).to eq(project.id)
+ expect(json_response['import_status']).to eq('scheduled')
+ expect(json_response).to include("import_error")
+ end
+
+ it 'fails to fork if path is already taken' do
+ post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
+ post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' }
+
+ expect(response).to have_gitlab_http_status(409)
+ expect(json_response['message']['path']).to eq(['has already been taken'])
+ end
+
+ it 'accepts a name for the target project' do
+ post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq('My Random Project')
+ expect(json_response['path']).to eq(project.path)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ expect(json_response['namespace']['id']).to eq(user2.namespace.id)
+ expect(json_response['forked_from_project']['id']).to eq(project.id)
+ expect(json_response['import_status']).to eq('scheduled')
+ expect(json_response).to include("import_error")
+ end
+
+ it 'fails to fork if name is already taken' do
+ post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
+ post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' }
+
+ expect(response).to have_gitlab_http_status(409)
+ expect(json_response['message']['name']).to eq(['has already been taken'])
+ end
end
context 'when unauthenticated' do
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index ba948e37e2f..3a59052bb29 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -73,6 +73,22 @@ describe API::Release::Links do
expect(response).to have_gitlab_http_status(:ok)
end
end
+
+ context 'when project is public and the repository is private' do
+ let(:project) { create(:project, :repository, :public, :repository_private) }
+
+ it_behaves_like '403 response' do
+ let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
+ end
+
+ context 'when the release does not exists' do
+ let!(:release) { }
+
+ it_behaves_like '403 response' do
+ let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index d7ddd97e8c8..e6c235ca26e 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -417,7 +417,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
'ref' => job.ref,
'sha' => job.sha,
'before_sha' => job.before_sha,
- 'ref_type' => 'branch' }
+ 'ref_type' => 'branch',
+ 'refspecs' => %w[+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*],
+ 'depth' => 0 }
end
let(:expected_steps) do
@@ -489,6 +491,29 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(201)
expect(json_response['git_info']['ref_type']).to eq('tag')
end
+
+ context 'when GIT_DEPTH is specified' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
+ end
+
+ it 'specifies refspecs' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['refspecs']).to include("+refs/tags/#{job.ref}:refs/tags/#{job.ref}")
+ end
+ end
+
+ context 'when GIT_DEPTH is not specified' do
+ it 'specifies refspecs' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['refspecs'])
+ .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*')
+ end
+ end
end
context 'when job is made for branch' do
@@ -498,6 +523,55 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response).to have_gitlab_http_status(201)
expect(json_response['git_info']['ref_type']).to eq('branch')
end
+
+ context 'when GIT_DEPTH is specified' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
+ end
+
+ it 'specifies refspecs' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['refspecs']).to include("+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}")
+ end
+ end
+
+ context 'when GIT_DEPTH is not specified' do
+ it 'specifies refspecs' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['refspecs'])
+ .to contain_exactly('+refs/tags/*:refs/tags/*', '+refs/heads/*:refs/remotes/origin/*')
+ end
+ end
+ end
+
+ context 'when job is made for merge request' do
+ let(:pipeline) { create(:ci_pipeline_without_jobs, source: :merge_request, project: project, ref: 'feature', merge_request: merge_request) }
+ let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
+ let(:merge_request) { create(:merge_request) }
+
+ it 'sets branch as ref_type' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['ref_type']).to eq('branch')
+ end
+
+ context 'when GIT_DEPTH is specified' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: pipeline)
+ end
+
+ it 'returns the overwritten git depth for merge request refspecs' do
+ request_job
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['git_info']['depth']).to eq(1)
+ end
+ end
end
it 'updates runner info' do
@@ -526,6 +600,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(runner.reload.ip_address).to eq('123.222.123.222')
end
+ it "handles multiple X-Forwarded-For addresses" do
+ post api('/jobs/request'),
+ params: { token: runner.token },
+ headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222, 127.0.0.1' }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(runner.reload.ip_address).to eq('123.222.123.222')
+ end
+
context 'when concurrently updating a job' do
before do
expect_any_instance_of(Ci::Build).to receive(:run!)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 7f11c8c9fe8..5ca442bc448 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -241,7 +241,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- get api('/runners/9999', admin)
+ get api('/runners/0', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -394,7 +394,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- update_runner(9999, admin, description: 'test')
+ update_runner(0, admin, description: 'test')
expect(response).to have_gitlab_http_status(404)
end
@@ -468,7 +468,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- delete api('/runners/9999', admin)
+ delete api('/runners/0', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -573,7 +573,7 @@ describe API::Runners do
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/9999/jobs', admin)
+ get api('/runners/0/jobs', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -626,7 +626,7 @@ describe API::Runners do
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/9999/jobs', user)
+ get api('/runners/0/jobs', user)
expect(response).to have_gitlab_http_status(404)
end
@@ -857,7 +857,7 @@ describe API::Runners do
end
it 'returns 404 is runner is not found' do
- delete api("/projects/#{project.id}/runners/9999", user)
+ delete api("/projects/#{project.id}/runners/0", user)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 831f47debeb..c48ca832c85 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -126,7 +126,7 @@ describe API::Search do
context 'when group does not exist' do
it 'returns 404 error' do
- get api('/groups/9999/search', user), params: { scope: 'issues', search: 'awesome' }
+ get api('/groups/0/search', user), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(404)
end
@@ -222,7 +222,7 @@ describe API::Search do
context 'when project does not exist' do
it 'returns 404 error' do
- get api('/projects/9999/search', user), params: { scope: 'issues', search: 'awesome' }
+ get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index b381431306d..a879426589d 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -335,7 +335,7 @@ describe API::Users do
end
it "returns a 404 error if user id not found" do
- get api("/users/9999", user)
+ get api("/users/0", user)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -732,7 +732,7 @@ describe API::Users do
end
it "returns 404 for non-existing user" do
- put api("/users/999999", admin), params: { bio: 'update should fail' }
+ put api("/users/0", admin), params: { bio: 'update should fail' }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -836,7 +836,7 @@ describe API::Users do
end
it "returns 400 for invalid ID" do
- post api("/users/999999/keys", admin)
+ post api("/users/0/keys", admin)
expect(response).to have_gitlab_http_status(400)
end
end
@@ -895,7 +895,7 @@ describe API::Users do
it 'returns 404 error if user not found' do
user.keys << key
user.save
- delete api("/users/999999/keys/#{key.id}", admin)
+ delete api("/users/0/keys/#{key.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -930,7 +930,7 @@ describe API::Users do
end
it 'returns 400 for invalid ID' do
- post api('/users/999999/gpg_keys', admin)
+ post api('/users/0/gpg_keys', admin)
expect(response).to have_gitlab_http_status(400)
end
@@ -951,7 +951,7 @@ describe API::Users do
context 'when authenticated' do
it 'returns 404 for non-existing user' do
- get api('/users/999999/gpg_keys', admin)
+ get api('/users/0/gpg_keys', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1007,7 +1007,7 @@ describe API::Users do
user.keys << key
user.save
- delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
+ delete api("/users/0/gpg_keys/#{gpg_key.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1051,7 +1051,7 @@ describe API::Users do
user.gpg_keys << gpg_key
user.save
- post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
+ post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1089,7 +1089,7 @@ describe API::Users do
end
it "returns a 400 for invalid ID" do
- post api("/users/999999/emails", admin)
+ post api("/users/0/emails", admin)
expect(response).to have_gitlab_http_status(400)
end
@@ -1121,7 +1121,7 @@ describe API::Users do
context 'when authenticated' do
it 'returns 404 for non-existing user' do
- get api('/users/999999/emails', admin)
+ get api('/users/0/emails', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1177,7 +1177,7 @@ describe API::Users do
it 'returns 404 error if user not found' do
user.emails << email
user.save
- delete api("/users/999999/emails/#{email.id}", admin)
+ delete api("/users/0/emails/#{email.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1227,7 +1227,7 @@ describe API::Users do
end
it "returns 404 for non-existing user" do
- perform_enqueued_jobs { delete api("/users/999999", admin) }
+ perform_enqueued_jobs { delete api("/users/0", admin) }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1778,7 +1778,7 @@ describe API::Users do
end
it 'returns a 404 error if user id not found' do
- post api('/users/9999/block', admin)
+ post api('/users/0/block', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1816,7 +1816,7 @@ describe API::Users do
end
it 'returns a 404 error if user id not found' do
- post api('/users/9999/block', admin)
+ post api('/users/0/block', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 5b625fd47be..bfa178f5cae 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -104,6 +104,70 @@ describe 'Git HTTP requests' do
end
end
+ shared_examples_for 'project path without .git suffix' do
+ context "GET info/refs" do
+ let(:path) { "/#{project_path}/info/refs" }
+
+ context "when no params are added" do
+ before do
+ get path
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs")
+ end
+ end
+
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+
+ context "POST git-receive-pack" do
+ it "fails to find a route" do
+ expect { push_post(project_path) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+ end
+
describe "User with no identities" do
let(:user) { create(:user) }
@@ -143,6 +207,10 @@ describe 'Git HTTP requests' do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
+
+ it_behaves_like 'project path without .git suffix' do
+ let(:project_path) { "#{user.namespace.path}/project.git-project" }
+ end
end
end
@@ -706,70 +774,8 @@ describe 'Git HTTP requests' do
end
end
- context "when the project path doesn't end in .git" do
- let(:project) { create(:project, :repository, :public, path: 'project.git-project') }
-
- context "GET info/refs" do
- let(:path) { "/#{project.full_path}/info/refs" }
-
- context "when no params are added" do
- before do
- get path
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs")
- end
- end
-
- context "when the upload-pack service is requested" do
- let(:params) { { service: 'git-upload-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}")
- end
- end
-
- context "when the receive-pack service is requested" do
- let(:params) { { service: 'git-receive-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}")
- end
- end
-
- context "when the params are anything else" do
- let(:params) { { service: 'git-implode-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the sign-in page" do
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "POST git-upload-pack" do
- it "fails to find a route" do
- expect { clone_post(project.full_path) }.to raise_error(ActionController::RoutingError)
- end
- end
-
- context "POST git-receive-pack" do
- it "fails to find a route" do
- expect { push_post(project.full_path) }.to raise_error(ActionController::RoutingError)
- end
- end
+ it_behaves_like 'project path without .git suffix' do
+ let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path }
end
context "retrieving an info/refs file" do
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 78ff9c6e6fd..106f92082e4 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -23,6 +23,11 @@ require 'spec_helper'
# end
shared_examples 'importer routing' do
let(:except_actions) { [] }
+ let(:is_realtime) { false }
+
+ before do
+ except_actions.push(is_realtime ? :jobs : :realtime_changes)
+ end
it 'to #create' do
expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create)
@@ -43,17 +48,22 @@ shared_examples 'importer routing' do
it 'to #jobs' do
expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs)
end
+
+ it 'to #realtime_changes' do
+ expect(get("/import/#{provider}/realtime_changes")).to route_to("import/#{provider}#realtime_changes") unless except_actions.include?(:realtime_changes)
+ end
end
# personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token
# status_import_github GET /import/github/status(.:format) import/github#status
# callback_import_github GET /import/github/callback(.:format) import/github#callback
-# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs
+# realtime_changes_import_github GET /import/github/realtime_changes(.:format) import/github#jobs
# import_github POST /import/github(.:format) import/github#create
# new_import_github GET /import/github/new(.:format) import/github#new
describe Import::GithubController, 'routing' do
it_behaves_like 'importer routing' do
let(:provider) { 'github' }
+ let(:is_realtime) { true }
end
it 'to #personal_access_token' do
@@ -63,13 +73,14 @@ end
# personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token
# status_import_gitea GET /import/gitea/status(.:format) import/gitea#status
-# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs
+# realtime_changes_import_gitea GET /import/gitea/realtime_changes(.:format) import/gitea#jobs
# import_gitea POST /import/gitea(.:format) import/gitea#create
# new_import_gitea GET /import/gitea/new(.:format) import/gitea#new
describe Import::GiteaController, 'routing' do
it_behaves_like 'importer routing' do
let(:except_actions) { [:callback] }
let(:provider) { 'gitea' }
+ let(:is_realtime) { true }
end
it 'to #personal_access_token' do
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
index 073c13c2cbb..92b649f5b6c 100644
--- a/spec/serializers/diff_file_entity_spec.rb
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -22,7 +22,7 @@ describe DiffFileEntity do
let(:request) { EntityRequest.new(project: project, current_user: user) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) }
- let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) }
+ let(:exposed_urls) { %i(edit_path view_path context_lines_path) }
it_behaves_like 'diff file entity'
@@ -38,6 +38,12 @@ describe DiffFileEntity do
expect(response[attribute]).to include(merge_request.target_project.to_param)
end
end
+
+ it 'exposes load_collapsed_diff_url if the file viewer is collapsed' do
+ allow(diff_file.viewer).to receive(:collapsed?) { true }
+
+ expect(subject).to include(:load_collapsed_diff_url)
+ end
end
context '#parallel_diff_lines' do
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 3541bd5f12e..375a28a8c72 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -124,10 +124,10 @@ describe EnvironmentSerializer do
end
context 'when used with pagination' do
- let(:request) { spy('request') }
+ let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) }
let(:response) { spy('response') }
let(:resource) { Environment.all }
- let(:pagination) { { page: 1, per_page: 2 } }
+ let(:query) { { page: 1, per_page: 2 } }
let(:serializer) do
described_class
@@ -135,11 +135,6 @@ describe EnvironmentSerializer do
.with_pagination(request, response)
end
- before do
- allow(request).to receive(:query_parameters)
- .and_return(pagination)
- end
-
subject { serializer.represent(resource) }
it 'creates a paginated serializer' do
diff --git a/spec/serializers/namespace_basic_entity_spec.rb b/spec/serializers/namespace_basic_entity_spec.rb
new file mode 100644
index 00000000000..f8b71ceb9f3
--- /dev/null
+++ b/spec/serializers/namespace_basic_entity_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NamespaceBasicEntity do
+ set(:group) { create(:group) }
+ let(:entity) do
+ described_class.represent(group)
+ end
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'includes required fields' do
+ expect(subject).to include :id, :full_path
+ end
+ end
+end
diff --git a/spec/serializers/namespace_serializer_spec.rb b/spec/serializers/namespace_serializer_spec.rb
new file mode 100644
index 00000000000..6e5bdd8c52d
--- /dev/null
+++ b/spec/serializers/namespace_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe NamespaceSerializer do
+ it 'represents NamespaceBasicEntity entities' do
+ expect(described_class.entity_class).to eq(NamespaceBasicEntity)
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 79aa32b29bb..2bdcb2a45f6 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -38,15 +38,9 @@ describe PipelineSerializer do
end
context 'when used with pagination' do
- let(:request) { spy('request') }
+ let(:request) { double(url: "#{Gitlab.config.gitlab.url}:8080/api/v4/projects?#{query.to_query}", query_parameters: query) }
let(:response) { spy('response') }
- let(:pagination) { {} }
-
- before do
- allow(request)
- .to receive(:query_parameters)
- .and_return(pagination)
- end
+ let(:query) { {} }
let(:serializer) do
described_class.new(current_user: user)
@@ -60,7 +54,7 @@ describe PipelineSerializer do
context 'when resource is not paginatable' do
context 'when a single pipeline object is being serialized' do
let(:resource) { create(:ci_empty_pipeline) }
- let(:pagination) { { page: 1, per_page: 1 } }
+ let(:query) { { page: 1, per_page: 1 } }
it 'raises error' do
expect { subject }.to raise_error(
@@ -71,7 +65,7 @@ describe PipelineSerializer do
context 'when resource is paginatable relation' do
let(:resource) { Ci::Pipeline.all }
- let(:pagination) { { page: 1, per_page: 2 } }
+ let(:query) { { page: 1, per_page: 2 } }
context 'when a single pipeline object is present in relation' do
before do
diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb
new file mode 100644
index 00000000000..e476da82729
--- /dev/null
+++ b/spec/serializers/project_import_entity_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectImportEntity do
+ include ImportHelper
+
+ set(:project) { create(:project, import_status: :started, import_source: 'namespace/project') }
+ let(:provider_url) { 'https://provider.com' }
+ let(:entity) { described_class.represent(project, provider_url: provider_url) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'includes required fields' do
+ expect(subject[:import_source]).to eq(project.import_source)
+ expect(subject[:import_status]).to eq(project.import_status)
+ expect(subject[:human_import_status_name]).to eq(project.human_import_status_name)
+ expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source]))
+ end
+ end
+end
diff --git a/spec/serializers/project_serializer_spec.rb b/spec/serializers/project_serializer_spec.rb
new file mode 100644
index 00000000000..22f958fc17f
--- /dev/null
+++ b/spec/serializers/project_serializer_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectSerializer do
+ set(:project) { create(:project) }
+ let(:provider_url) { 'http://provider.com' }
+
+ context 'when serializer option is :import' do
+ subject do
+ described_class.new.represent(project, serializer: :import, provider_url: provider_url)
+ end
+
+ before do
+ allow(ProjectImportEntity).to receive(:represent)
+ end
+
+ it 'represents with ProjectImportEntity' do
+ subject
+
+ expect(ProjectImportEntity)
+ .to have_received(:represent)
+ .with(project, serializer: :import, provider_url: provider_url, request: an_instance_of(EntityRequest))
+ end
+ end
+
+ context 'when serializer option is omitted' do
+ subject do
+ described_class.new.represent(project)
+ end
+
+ before do
+ allow(ProjectEntity).to receive(:represent)
+ end
+
+ it 'represents with ProjectEntity' do
+ subject
+
+ expect(ProjectEntity)
+ .to have_received(:represent)
+ .with(project, request: an_instance_of(EntityRequest))
+ end
+ end
+end
diff --git a/spec/serializers/provider_repo_entity_spec.rb b/spec/serializers/provider_repo_entity_spec.rb
new file mode 100644
index 00000000000..b67115bab10
--- /dev/null
+++ b/spec/serializers/provider_repo_entity_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProviderRepoEntity do
+ include ImportHelper
+
+ let(:provider_repo) { { id: 1, full_name: 'full/name', name: 'name', owner: { login: 'owner' } } }
+ let(:provider) { :github }
+ let(:provider_url) { 'https://github.com' }
+ let(:entity) { described_class.represent(provider_repo, provider: provider, provider_url: provider_url) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'includes requried fields' do
+ expect(subject[:id]).to eq(provider_repo[:id])
+ expect(subject[:full_name]).to eq(provider_repo[:full_name])
+ expect(subject[:owner_name]).to eq(provider_repo[:owner][:login])
+ expect(subject[:sanitized_name]).to eq(sanitize_project_name(provider_repo[:name]))
+ expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, provider_repo[:full_name]))
+ end
+ end
+end
diff --git a/spec/serializers/provider_repo_serializer_spec.rb b/spec/serializers/provider_repo_serializer_spec.rb
new file mode 100644
index 00000000000..f2be30c36d9
--- /dev/null
+++ b/spec/serializers/provider_repo_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProviderRepoSerializer do
+ it 'represents ProviderRepoEntity entities' do
+ expect(described_class.entity_class).to eq(ProviderRepoEntity)
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8497e90bd8b..93349ba7b5b 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -12,6 +12,7 @@ describe Ci::CreatePipelineService do
end
describe '#execute' do
+ # rubocop:disable Metrics/ParameterLists
def execute_service(
source: :push,
after: project.commit.id,
@@ -20,17 +21,22 @@ describe Ci::CreatePipelineService do
trigger_request: nil,
variables_attributes: nil,
merge_request: nil,
- push_options: nil)
+ push_options: nil,
+ source_sha: nil,
+ target_sha: nil)
params = { ref: ref,
before: '00000000',
after: after,
commits: [{ message: message }],
variables_attributes: variables_attributes,
- push_options: push_options }
+ push_options: push_options,
+ source_sha: source_sha,
+ target_sha: target_sha }
described_class.new(project, user, params).execute(
source, trigger_request: trigger_request, merge_request: merge_request)
end
+ # rubocop:enable Metrics/ParameterLists
context 'valid params' do
let(:pipeline) { execute_service }
@@ -679,7 +685,11 @@ describe Ci::CreatePipelineService do
describe 'Merge request pipelines' do
let(:pipeline) do
- execute_service(source: source, merge_request: merge_request, ref: ref_name)
+ execute_service(source: source,
+ merge_request: merge_request,
+ ref: ref_name,
+ source_sha: source_sha,
+ target_sha: target_sha)
end
before do
@@ -687,6 +697,8 @@ describe Ci::CreatePipelineService do
end
let(:ref_name) { 'refs/heads/feature' }
+ let(:source_sha) { project.commit(ref_name).id }
+ let(:target_sha) { nil }
context 'when source is merge request' do
let(:source) { :merge_request }
@@ -727,6 +739,22 @@ describe Ci::CreatePipelineService do
expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test])
end
+ it 'persists the specified source sha' do
+ expect(pipeline.source_sha).to eq(source_sha)
+ end
+
+ it 'does not persist target sha for detached merge request pipeline' do
+ expect(pipeline.target_sha).to be_nil
+ end
+
+ context 'when target sha is specified' do
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it 'persists the target sha' do
+ expect(pipeline.target_sha).to eq(target_sha)
+ end
+ end
+
context 'when ref is tag' do
let(:ref_name) { 'refs/tags/v1.1.0' }
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 3f621ed5944..cbdef008b07 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -26,12 +26,6 @@ describe Clusters::Applications::CreateService do
end.to change(cluster, :application_helm)
end
- it 'schedules an install via worker' do
- expect(ClusterInstallAppWorker).to receive(:perform_async).with('helm', anything).once
-
- subject
- end
-
context 'application already installed' do
let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) }
@@ -42,88 +36,101 @@ describe Clusters::Applications::CreateService do
end
it 'schedules an upgrade for the application' do
- expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original
+ expect(ClusterUpgradeAppWorker).to receive(:perform_async)
subject
end
end
- context 'cert manager application' do
- let(:params) do
- {
- application: 'cert_manager',
- email: 'test@example.com'
- }
- end
-
+ context 'known applications' do
before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ create(:clusters_applications_helm, :installed, cluster: cluster)
end
- it 'creates the application' do
- expect do
- subject
+ context 'cert manager application' do
+ let(:params) do
+ {
+ application: 'cert_manager',
+ email: 'test@example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_cert_manager)
- end
+ before do
+ expect_any_instance_of(Clusters::Applications::CertManager)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the email' do
- expect(subject.email).to eq('test@example.com')
- end
- end
+ it 'creates the application' do
+ expect do
+ subject
- context 'jupyter application' do
- let(:params) do
- {
- application: 'jupyter',
- hostname: 'example.com'
- }
- end
+ cluster.reload
+ end.to change(cluster, :application_cert_manager)
+ end
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ it 'sets the email' do
+ expect(subject.email).to eq('test@example.com')
+ end
end
- it 'creates the application' do
- expect do
- subject
+ context 'jupyter application' do
+ let(:params) do
+ {
+ application: 'jupyter',
+ hostname: 'example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_jupyter)
- end
+ before do
+ create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
+ expect_any_instance_of(Clusters::Applications::Jupyter)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
+ it 'creates the application' do
+ expect do
+ subject
- it 'sets the oauth_application' do
- expect(subject.oauth_application).to be_present
- end
- end
+ cluster.reload
+ end.to change(cluster, :application_jupyter)
+ end
- context 'knative application' do
- let(:params) do
- {
- application: 'knative',
- hostname: 'example.com'
- }
- end
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ it 'sets the oauth_application' do
+ expect(subject.oauth_application).to be_present
+ end
end
- it 'creates the application' do
- expect do
- subject
+ context 'knative application' do
+ let(:params) do
+ {
+ application: 'knative',
+ hostname: 'example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_knative)
- end
+ before do
+ expect_any_instance_of(Clusters::Applications::Knative)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_knative)
+ end
+
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
end
end
@@ -140,19 +147,21 @@ describe Clusters::Applications::CreateService do
using RSpec::Parameterized::TableSyntax
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
- end
-
- where(:application, :association, :allowed) do
- 'helm' | :application_helm | true
- 'ingress' | :application_ingress | true
- 'runner' | :application_runner | false
- 'jupyter' | :application_jupyter | false
- 'prometheus' | :application_prometheus | false
+ where(:application, :association, :allowed, :pre_create_helm) do
+ 'helm' | :application_helm | true | false
+ 'ingress' | :application_ingress | true | true
+ 'runner' | :application_runner | false | true
+ 'jupyter' | :application_jupyter | false | true
+ 'prometheus' | :application_prometheus | false | true
end
with_them do
+ before do
+ klass = "Clusters::Applications::#{application.titleize}"
+ allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original
+ create(:clusters_applications_helm, :installed, cluster: cluster) if pre_create_helm
+ end
+
let(:params) { { application: application } }
it 'executes for each application' do
@@ -168,5 +177,68 @@ describe Clusters::Applications::CreateService do
end
end
end
+
+ context 'when application is installable' do
+ shared_examples 'installable applications' do
+ it 'makes the application scheduled' do
+ expect do
+ subject
+ end.to change { Clusters::Applications::Helm.with_status(:scheduled).count }.by(1)
+ end
+
+ it 'schedules an install via worker' do
+ expect(ClusterInstallAppWorker)
+ .to receive(:perform_async)
+ .with(*worker_arguments)
+ .once
+
+ subject
+ end
+ end
+
+ context 'when application is associated with a cluster' do
+ let(:application) { create(:clusters_applications_helm, :installable, cluster: cluster) }
+ let(:worker_arguments) { [application.name, application.id] }
+
+ it_behaves_like 'installable applications'
+ end
+
+ context 'when application is not associated with a cluster' do
+ let(:worker_arguments) { [params[:application], kind_of(Numeric)] }
+
+ it_behaves_like 'installable applications'
+ end
+ end
+
+ context 'when installation is already in progress' do
+ let!(:application) { create(:clusters_applications_helm, :installing, cluster: cluster) }
+
+ it 'raises an exception' do
+ expect { subject }
+ .to raise_exception(StateMachines::InvalidTransition)
+ .and not_change(application.class.with_status(:scheduled), :count)
+ end
+
+ it 'does not schedule a cluster worker' do
+ expect(ClusterInstallAppWorker).not_to receive(:perform_async)
+ end
+ end
+
+ context 'when application is installed' do
+ %i(installed updated).each do |status|
+ let(:application) { create(:clusters_applications_helm, status, cluster: cluster) }
+
+ it 'schedules an upgrade via worker' do
+ expect(ClusterUpgradeAppWorker)
+ .to receive(:perform_async)
+ .with(application.name, application.id)
+ .once
+
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+ end
+ end
end
end
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
deleted file mode 100644
index 8380932dfaa..00000000000
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe Clusters::Applications::ScheduleInstallationService do
- def count_scheduled
- application&.class&.with_status(:scheduled)&.count || 0
- end
-
- shared_examples 'a failing service' do
- it 'raise an exception' do
- expect(ClusterInstallAppWorker).not_to receive(:perform_async)
- count_before = count_scheduled
-
- expect { service.execute }.to raise_error(StandardError)
- expect(count_scheduled).to eq(count_before)
- end
- end
-
- describe '#execute' do
- let(:service) { described_class.new(application) }
-
- context 'when application is installable' do
- let(:application) { create(:clusters_applications_helm, :installable) }
-
- it 'make the application scheduled' do
- expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once
-
- expect { service.execute }.to change { application.class.with_status(:scheduled).count }.by(1)
- end
- end
-
- context 'when installation is already in progress' do
- let(:application) { create(:clusters_applications_helm, :installing) }
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application is nil' do
- let(:application) { nil }
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application cannot be persisted' do
- let(:application) { create(:clusters_applications_helm) }
-
- before do
- expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
- end
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application is installed' do
- let(:application) { create(:clusters_applications_helm, :installed) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
-
- service.execute
-
- expect(application).to be_scheduled
- end
- end
-
- context 'when application is updated' do
- let(:application) { create(:clusters_applications_helm, :updated) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
-
- service.execute
-
- expect(application).to be_scheduled
- end
- end
- end
-end
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index d9dab1d705c..9d4fc62f923 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -45,7 +45,23 @@ describe ErrorTracking::ListIssuesService do
it 'result is not ready' do
expect(result).to eq(
- status: :error, http_status: :no_content, message: 'not ready')
+ status: :error, http_status: :no_content, message: 'Not ready. Try again later')
+ end
+ end
+
+ context 'when list_sentry_issues returns error' do
+ before do
+ allow(error_tracking_setting)
+ .to receive(:list_sentry_issues)
+ .and_return(error: 'Sentry response status code: 401')
+ end
+
+ it 'returns the error' do
+ expect(result).to eq(
+ status: :error,
+ http_status: :bad_request,
+ message: 'Sentry response status code: 401'
+ )
end
end
end
@@ -58,7 +74,11 @@ describe ErrorTracking::ListIssuesService do
it 'returns error' do
result = subject.execute
- expect(result).to include(status: :error, message: 'access denied')
+ expect(result).to include(
+ status: :error,
+ message: 'Access denied',
+ http_status: :unauthorized
+ )
end
end
@@ -70,7 +90,7 @@ describe ErrorTracking::ListIssuesService do
it 'raises error' do
result = subject.execute
- expect(result).to include(status: :error, message: 'not enabled')
+ expect(result).to include(status: :error, message: 'Error Tracking is not enabled')
end
end
end
diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb
index ee9c59e3f65..9f25a633deb 100644
--- a/spec/services/error_tracking/list_projects_service_spec.rb
+++ b/spec/services/error_tracking/list_projects_service_spec.rb
@@ -53,11 +53,11 @@ describe ErrorTracking::ListProjectsService do
context 'sentry client raises exception' do
before do
expect(error_tracking_setting).to receive(:list_sentry_projects)
- .and_raise(Sentry::Client::Error, 'Sentry response error: 500')
+ .and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
end
it 'returns error response' do
- expect(result[:message]).to eq('Sentry response error: 500')
+ expect(result[:message]).to eq('Sentry response status code: 500')
expect(result[:http_status]).to eq(:bad_request)
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 04a62aa454d..ede79b87bcc 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -224,6 +224,18 @@ describe MergeRequests::MergeService do
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+ it 'logs and saves error if user is not authorized' do
+ unauthorized_user = create(:user)
+ project.add_reporter(unauthorized_user)
+
+ service = described_class.new(project, unauthorized_user)
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error)
+ .to eq('You are not allowed to merge this merge request')
+ end
+
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
new file mode 100644
index 00000000000..96f2fde7117
--- /dev/null
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MergeToRefService do
+ shared_examples_for 'MergeService for target ref' do
+ it 'target_ref has the same state of target branch' do
+ repo = merge_request.target_project.repository
+
+ process_merge_to_ref
+ merge_service.execute(merge_request)
+
+ ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3)
+ target_branch_commits = repo.commits(merge_request.target_branch, limit: 3)
+
+ ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit|
+ expect(ref_commit.parents).to eq(target_branch_commit.parents)
+ end
+ end
+ end
+
+ set(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, :simple) }
+ let(:project) { merge_request.project }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe '#execute' do
+ let(:service) do
+ described_class.new(project, user,
+ commit_message: 'Awesome message',
+ 'should_remove_source_branch' => true)
+ end
+
+ def process_merge_to_ref
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ it 'writes commit to merge ref' do
+ repository = project.repository
+ target_ref = merge_request.merge_ref_path
+
+ expect(repository.ref_exists?(target_ref)).to be(false)
+
+ result = service.execute(merge_request)
+
+ ref_head = repository.commit(target_ref)
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:commit_id]).to be_present
+ expect(repository.ref_exists?(target_ref)).to be(true)
+ expect(ref_head.id).to eq(result[:commit_id])
+ end
+
+ it 'does not send any mail' do
+ expect { process_merge_to_ref }.not_to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it 'does not change the MR state' do
+ expect { process_merge_to_ref }.not_to change { merge_request.state }
+ end
+
+ it 'does not create notes' do
+ expect { process_merge_to_ref }.not_to change { merge_request.notes.count }
+ end
+
+ it 'does not delete the source branch' do
+ expect(DeleteBranchService).not_to receive(:new)
+
+ process_merge_to_ref
+ end
+
+ it 'returns error when feature is disabled' do
+ stub_feature_flags(merge_to_tmp_merge_ref_path: false)
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Feature is not enabled')
+ end
+
+ it 'returns an error when the failing to process the merge' do
+ allow(project.repository).to receive(:merge_to_ref).and_return(nil)
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Conflicts detected during merge')
+ end
+
+ context 'commit history comparison with regular MergeService' do
+ let(:merge_ref_service) do
+ described_class.new(project, user, {})
+ end
+
+ let(:merge_service) do
+ MergeRequests::MergeService.new(project, user, {})
+ end
+
+ context 'when merge commit' do
+ it_behaves_like 'MergeService for target ref'
+ end
+
+ context 'when merge commit with squash' do
+ before do
+ merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature')
+ end
+
+ it_behaves_like 'MergeService for target ref'
+ end
+ end
+
+ context 'merge pre-condition checks' do
+ before do
+ merge_request.project.update!(merge_method: merge_method)
+ end
+
+ context 'when semi-linear merge method' do
+ let(:merge_method) { :rebase_merge }
+
+ it 'return error when MR should be able to fast-forward' do
+ allow(merge_request).to receive(:should_be_rebased?) { true }
+
+ error_message = 'Fast-forward merge is not possible. Please update your source branch.'
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
+ end
+
+ context 'when fast-forward merge method' do
+ let(:merge_method) { :ff }
+
+ it 'returns error' do
+ error_message = "Fast-forward to #{merge_request.merge_ref_path} is currently not supported."
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
+ end
+
+ context 'when MR is not mergeable to ref' do
+ let(:merge_method) { :merge }
+
+ it 'returns error' do
+ allow(merge_request).to receive(:mergeable_to_ref?) { false }
+
+ error_message = "Merge request is not mergeable to #{merge_request.merge_ref_path}"
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
+ end
+ end
+
+ context 'does not close related todos' do
+ let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:project) { merge_request.project }
+ let!(:todo) do
+ create(:todo, :assigned,
+ project: project,
+ author: user,
+ user: user,
+ target: merge_request)
+ end
+
+ before do
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ todo.reload
+ end
+ end
+
+ it { expect(todo).not_to be_done }
+ end
+
+ it 'returns error when user has no authorization to admin the merge request' do
+ unauthorized_user = create(:user)
+ project.add_reporter(unauthorized_user)
+
+ service = described_class.new(project, unauthorized_user)
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('You are not allowed to merge to this ref')
+ end
+ end
+end
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index ffb270d277e..68fd82b4cbe 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -12,6 +12,10 @@ describe Projects::GroupLinks::CreateService, '#execute' do
end
let(:subject) { described_class.new(project, user, opts) }
+ before do
+ group.add_developer(user)
+ end
+
it 'adds group to project' do
expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1)
end
@@ -19,4 +23,8 @@ describe Projects::GroupLinks::CreateService, '#execute' do
it 'returns false if group is blank' do
expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
end
+
+ it 'returns error if user is not allowed to share with a group' do
+ expect { subject.execute(create :group) }.not_to change { project.project_group_links.count }
+ end
end
diff --git a/spec/services/prometheus/adapter_service_spec.rb b/spec/services/prometheus/adapter_service_spec.rb
index 335fc5844aa..505e2935e93 100644
--- a/spec/services/prometheus/adapter_service_spec.rb
+++ b/spec/services/prometheus/adapter_service_spec.rb
@@ -22,7 +22,15 @@ describe Prometheus::AdapterService do
context "prometheus service can't execute queries" do
let(:prometheus_service) { double(:prometheus_service, can_query?: false) }
- context 'with cluster with prometheus installed' do
+ context 'with cluster with prometheus not available' do
+ let!(:prometheus) { create(:clusters_applications_prometheus, :installable, cluster: cluster) }
+
+ it 'returns nil' do
+ expect(subject.prometheus_adapter).to be_nil
+ end
+ end
+
+ context 'with cluster with prometheus available' do
let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
it 'returns application handling all environments' do
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 719b4adf212..3c0a4ac8e18 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -26,6 +26,12 @@ describe Users::ActivityService do
.from(last_activity_on)
.to(Date.today)
end
+
+ it 'tries to obtain ExclusiveLease' do
+ expect(Gitlab::ExclusiveLease).to receive(:new).and_call_original
+
+ subject.execute
+ end
end
context 'when a bad object is passed' do
@@ -46,6 +52,12 @@ describe Users::ActivityService do
it 'does not update last_activity_on' do
expect { subject.execute }.not_to change(user, :last_activity_on)
end
+
+ it 'does not try to obtain ExclusiveLease' do
+ expect(Gitlab::ExclusiveLease).not_to receive(:new)
+
+ subject.execute
+ end
end
context 'when in GitLab read-only instance' do
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 5945a7dc0ad..747e04fb18c 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -102,7 +102,7 @@ describe WebHookService do
exception = exception_class.new('Exception message')
WebMock.stub_request(:post, project_hook.url).to_raise(exception)
- expect(service_instance.execute).to eq({ status: :error, message: exception.message })
+ expect(service_instance.execute).to eq({ status: :error, message: exception.to_s })
expect { service_instance.execute }.not_to raise_error
end
end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index b426fadb001..5f709831ce1 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -8,17 +8,12 @@ shared_examples_for 'group and project milestones' do |route_definition|
describe "GET #{route_definition}" do
it 'returns milestones list' do
- create(:issue, project: project, milestone: milestone)
- create(:closed_issue, project: project, milestone: milestone)
- create(:closed_issue, project: project, milestone: milestone)
-
get api(route, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
- expect(json_response.first['percentage_complete']).to eq(66)
end
it 'returns a 401 error if user not authenticated' do
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 697f999e4c4..5bb1269a19d 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -58,36 +58,54 @@ end
shared_examples 'a GitHub-ish import controller: GET status' do
let(:new_import_url) { public_send("new_import_#{provider}_url") }
let(:user) { create(:user) }
- let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') }
+ let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) }
let(:org) { OpenStruct.new(login: 'company') }
- let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') }
- let(:extra_assign_expectations) { {} }
+ let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) }
before do
assign_session_token(provider)
end
- it "assigns variables" do
- project = create(:project, import_type: provider, namespace: user.namespace)
+ it "returns variables for json request" do
+ project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo')
+ group = create(:group)
+ group.add_owner(user)
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
- get :status
+ get :status, format: :json
- expect(assigns(:already_added_projects)).to eq([project])
- expect(assigns(:repos)).to eq([repo, org_repo])
- extra_assign_expectations.each do |key, value|
- expect(assigns(key)).to eq(value)
- end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos", 0, "id")).to eq(repo.id)
+ expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id)
+ expect(json_response.dig("namespaces", 0, "id")).to eq(group.id)
end
it "does not show already added project" do
- project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim')
+ project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: [])
+ get :status, format: :json
+
+ expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id)
+ expect(json_response.dig("provider_repos")).to eq([])
+ end
+
+ it "touches the etag cache store" do
+ expect(stub_client(repos: [], orgs: [])).to receive(:repos)
+ expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
+ expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
+ end
+
+ get :status, format: :json
+ end
+
+ it "requests provider repos list" do
+ expect(stub_client(repos: [], orgs: [])).to receive(:repos)
+
get :status
- expect(assigns(:already_added_projects)).to eq([project])
- expect(assigns(:repos)).to eq([])
+ expect(response).to have_gitlab_http_status(200)
end
it "handles an invalid access token" do
@@ -100,13 +118,32 @@ shared_examples 'a GitHub-ish import controller: GET status' do
expect(controller).to redirect_to(new_import_url)
expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.")
end
+
+ it "does not produce N+1 database queries" do
+ stub_client(repos: [repo], orgs: [])
+ group_a = create(:group)
+ group_a.add_owner(user)
+ create(:project, :import_started, import_type: provider, namespace: user.namespace)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get :status, format: :json
+ end.count
+
+ stub_client(repos: [repo, org_repo], orgs: [])
+ group_b = create(:group)
+ group_b.add_owner(user)
+ create(:project, :import_started, import_type: provider, namespace: user.namespace)
+
+ expect { get :status, format: :json }
+ .not_to exceed_all_query_limit(control_count)
+ end
end
shared_examples 'a GitHub-ish import controller: POST create' do
let(:user) { create(:user) }
- let(:project) { create(:project) }
let(:provider_username) { user.username }
let(:provider_user) { OpenStruct.new(login: provider_username) }
+ let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") }
let(:provider_repo) do
OpenStruct.new(
name: 'vim',
@@ -145,6 +182,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do
expect(json_response['errors']).to eq('Name is invalid, Path is old')
end
+ it "touches the etag cache store" do
+ allow(Gitlab::LegacyGithubImport::ProjectCreator)
+ .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
+ .and_return(double(execute: project))
+ expect_next_instance_of(Gitlab::EtagCaching::Store) do |store|
+ expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" }
+ end
+
+ post :create, format: :json
+ end
+
context "when the repository owner is the provider user" do
context "when the provider user and GitLab user's usernames match" do
it "takes the current user's namespace" do
@@ -351,7 +399,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not create a new namespace under the user namespace' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
- .and_return(double(execute: build_stubbed(:project)))
+ .and_return(double(execute: project))
expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js }
.not_to change { Namespace.count }
@@ -365,7 +413,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not take the selected namespace and name' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider)
- .and_return(double(execute: build_stubbed(:project)))
+ .and_return(double(execute: project))
post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js
end
@@ -373,7 +421,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it 'does not create the namespaces' do
allow(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider)
- .and_return(double(execute: build_stubbed(:project)))
+ .and_return(double(execute: project))
expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js }
.not_to change { Namespace.count }
@@ -390,7 +438,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
expect(Gitlab::LegacyGithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider)
- .and_return(double(execute: build_stubbed(:project)))
+ .and_return(double(execute: project))
post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js
end
@@ -407,3 +455,20 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
end
end
+
+shared_examples 'a GitHub-ish import controller: GET realtime_changes' do
+ let(:user) { create(:user) }
+
+ before do
+ assign_session_token(provider)
+ end
+
+ it 'sets a Poll-Interval header' do
+ project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo')
+
+ get :realtime_changes
+
+ expect(json_response).to eq([{ "id" => project.id, "import_status" => project.import_status }])
+ expect(Integer(response.headers['Poll-Interval'])).to be > -1
+ end
+end
diff --git a/spec/support/helpers/file_mover_helpers.rb b/spec/support/helpers/file_mover_helpers.rb
new file mode 100644
index 00000000000..1ba7cc03354
--- /dev/null
+++ b/spec/support/helpers/file_mover_helpers.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module FileMoverHelpers
+ def stub_file_mover(file_path, stub_real_path: nil)
+ file_name = File.basename(file_path)
+ allow(Pathname).to receive(:new).and_call_original
+
+ expect_next_instance_of(Pathname, a_string_including(file_name)) do |pathname|
+ allow(pathname).to receive(:realpath) { stub_real_path || pathname.cleanpath }
+ end
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index ea3a03879c5..e468ee4676d 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -84,7 +84,7 @@ module GraphqlHelpers
QUERY
end
- def all_graphql_fields_for(class_name)
+ def all_graphql_fields_for(class_name, parent_types = Set.new)
type = GitlabSchema.types[class_name.to_s]
return "" unless type
@@ -92,8 +92,17 @@ module GraphqlHelpers
# We can't guess arguments, so skip fields that require them
next if required_arguments?(field)
+ singular_field_type = field_type(field)
+
+ # If field type is the same as parent type, then we're hitting into
+ # mutual dependency. Break it from infinite recursion
+ next if parent_types.include?(singular_field_type)
+
if nested_fields?(field)
- "#{name} { #{all_graphql_fields_for(field_type(field))} }"
+ fields =
+ all_graphql_fields_for(singular_field_type, parent_types | [type])
+
+ "#{name} { #{fields} }"
else
name
end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 3fee6872498..4a0cf62a661 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -47,7 +47,7 @@ module LoginHelpers
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
- mock_auth_hash(provider, uid, user.email, saml_response)
+ mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_user_session_path
click_link provider
end
@@ -87,7 +87,12 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
- def mock_auth_hash(provider, uid, email, saml_response = nil)
+ def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response)
+ response_object = { document: saml_xml(saml_response) }
+ mock_auth_hash(provider, uid, email, response_object: response_object)
+ end
+
+ def mock_auth_hash(provider, uid, email, response_object: nil)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
@@ -110,9 +115,7 @@ module LoginHelpers
image: 'mock_user_thumbnail_url'
}
},
- response_object: {
- document: saml_xml(saml_response)
- }
+ response_object: response_object
}
})
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb
index a575aa99b79..b76b53db0b9 100644
--- a/spec/support/helpers/reactive_caching_helpers.rb
+++ b/spec/support/helpers/reactive_caching_helpers.rb
@@ -10,7 +10,7 @@ module ReactiveCachingHelpers
def stub_reactive_cache(subject = nil, data = nil, *qualifiers)
allow(ReactiveCachingWorker).to receive(:perform_async)
allow(ReactiveCachingWorker).to receive(:perform_in)
- write_reactive_cache(subject, data, *qualifiers) if data
+ write_reactive_cache(subject, data, *qualifiers) unless data.nil?
end
def synchronous_reactive_cache(subject)
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index f485eb7b0eb..06bcf4f8013 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -63,7 +63,8 @@ module TestEnv
'after-create-delete-modify-move' => 'ba3faa7',
'with-codeowners' => '219560e',
'submodule_inside_folder' => 'b491b92',
- 'png-lfs' => 'fe42f41'
+ 'png-lfs' => 'fe42f41',
+ 'sha-starting-with-large-number' => '8426165'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 7be84838e00..7894484f590 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -1,8 +1,6 @@
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
match do |field|
- field_definition = field.metadata[:type_class]
- expect(field_definition).to respond_to(:required_permissions)
- expect(field_definition.required_permissions).to contain_exactly(*expected)
+ expect(field.metadata[:authorize]).to eq(*expected)
end
end
diff --git a/spec/support/matchers/not_changed_matcher.rb b/spec/support/matchers/not_changed_matcher.rb
new file mode 100644
index 00000000000..8ef4694982d
--- /dev/null
+++ b/spec/support/matchers/not_changed_matcher.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define_negated_matcher :not_change, :change
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index 23f9b46ae0c..d92e8318fa0 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -19,7 +19,7 @@ Service.available_services_names.each do |service|
elsif service == 'irker' && k == :server_port
hash.merge!(k => 1234)
elsif service == 'jira' && k == :jira_issue_transition_id
- hash.merge!(k => 1234)
+ hash.merge!(k => '1,2,3')
else
hash.merge!(k => "someword")
end
diff --git a/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb
new file mode 100644
index 00000000000..713f0a879c1
--- /dev/null
+++ b/spec/support/shared_examples/graphql/issuable_state_shared_examples.rb
@@ -0,0 +1,5 @@
+RSpec.shared_examples 'issuable state' do
+ it 'exposes all the existing issuable states' do
+ expect(described_class.values.keys).to include(*%w[opened closed locked])
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index c96a65cb56a..b8c19cab0c4 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -9,6 +9,19 @@ shared_examples 'cluster application status specs' do |application_name|
end
end
+ describe '.available' do
+ subject { described_class.available }
+
+ let!(:installed_cluster) { create(application_name, :installed) }
+ let!(:updated_cluster) { create(application_name, :updated) }
+
+ before do
+ create(application_name, :errored)
+ end
+
+ it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
+ end
+
describe 'status state machine' do
describe '#make_installing' do
subject { create(application_name, :scheduled) }
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
index e44da4faa5a..eff8e401bad 100644
--- a/spec/support/shared_examples/requests/api/discussions.rb
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -86,6 +86,37 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'when a project is public with private repo access' do
+ let!(:parent) { create(:project, :public, :repository, :repository_private, :snippets_private) }
+ let!(:user_without_access) { create(:user) }
+
+ context 'when user is not a team member of private repo' do
+ before do
+ project.team.truncate
+ end
+
+ context "creating a new note" do
+ before do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user_without_access), params: { body: 'hi!' }
+ end
+
+ it 'raises 404 error' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context "fetching a discussion" do
+ before do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{note.discussion_id}", user_without_access)
+ end
+
+ it 'raises 404 error' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
end
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
index 453f42251c8..6713ec47ace 100644
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -257,6 +257,38 @@ shared_examples 'merge requests list' do
expect(response_dates).to eq(response_dates.sort.reverse)
end
+ context '2 merge requests with equal created_at' do
+ let!(:closed_mr2) do
+ create :merge_request,
+ state: 'closed',
+ milestone: milestone1,
+ author: user,
+ assignee: user,
+ source_project: project,
+ target_project: project,
+ title: "Test",
+ created_at: @mr_earlier.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).to include(closed_mr2.id)
+ expect(response_ids).not_to include(@mr_earlier.id)
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).not_to include(closed_mr2.id)
+ expect(response_ids).to include(@mr_earlier.id)
+ end
+ end
+
it 'returns an array of merge_requests ordered by updated_at' do
path = endpoint_path + '?order_by=updated_at'
diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb
index 71499e85654..57eefd5ef01 100644
--- a/spec/support/shared_examples/requests/api/notes.rb
+++ b/spec/support/shared_examples/requests/api/notes.rb
@@ -8,13 +8,45 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name|
create_list(:note, 3, params)
end
- it 'sorts by created_at in descending order by default' do
- get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
+ context 'without sort params' do
+ it 'sorts by created_at in descending order by default' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user)
- response_dates = json_response.map { |note| note['created_at'] }
+ response_dates = json_response.map { |note| note['created_at'] }
- expect(json_response.length).to eq(4)
- expect(response_dates).to eq(response_dates.sort.reverse)
+ expect(json_response.length).to eq(4)
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ context '2 notes with equal created_at' do
+ before do
+ @first_note = Note.first
+
+ params = { noteable: noteable, author: user }
+ params[:project] = parent if parent.is_a?(Project)
+ params[:created_at] = @first_note.created_at
+
+ @note2 = create(:note, params)
+ end
+
+ it 'page breaks first page correctly' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user)
+
+ response_ids = json_response.map { |note| note['id'] }
+
+ expect(response_ids).to include(@note2.id)
+ expect(response_ids).not_to include(@first_note.id)
+ end
+
+ it 'page breaks second page correctly' do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user)
+
+ response_ids = json_response.map { |note| note['id'] }
+
+ expect(response_ids).not_to include(@note2.id)
+ expect(response_ids).to include(@first_note.id)
+ end
+ end
end
it 'sorts by ascending order when requested' do
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
index 1770308f789..96cb71be737 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb
@@ -6,9 +6,9 @@ shared_examples 'diff file base entity' do
:submodule_tree_url, :old_path_html,
:new_path_html, :blob, :can_modify_blob,
:file_hash, :file_path, :old_path, :new_path,
- :collapsed, :text, :diff_refs, :stored_externally,
+ :viewer, :diff_refs, :stored_externally,
:external_storage, :renamed_file, :deleted_file,
- :mode_changed, :a_mode, :b_mode, :new_file)
+ :a_mode, :b_mode, :new_file)
end
# Converted diff files from GitHub import does not contain blob file
@@ -30,9 +30,9 @@ shared_examples 'diff file entity' do
it_behaves_like 'diff file base entity'
it 'exposes correct attributes' do
- expect(subject).to include(:too_large, :added_lines, :removed_lines,
+ expect(subject).to include(:added_lines, :removed_lines,
:context_lines_path, :highlighted_diff_lines,
- :parallel_diff_lines, :empty)
+ :parallel_diff_lines)
end
it 'includes viewer' do
diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb
index ec44b99d10e..9dbd1d8e867 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service.rb
@@ -39,6 +39,22 @@ shared_examples 'issues move service' do |group|
end
end
+ context 'when moving to backlog' do
+ let(:milestone) { create(:milestone, project: project) }
+ let!(:backlog) { create(:backlog_list, board: board1) }
+
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression], milestone: milestone) }
+ let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: backlog.id } }
+
+ it 'keeps labels and milestone' do
+ described_class.new(parent, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug, regression)
+ expect(issue.milestone).to eq(milestone)
+ end
+ end
+
context 'when moving from closed' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } }
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index de29d0c943f..e474a714b10 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe FileMover do
+ include FileMoverHelpers
+
let(:filename) { 'banana_sample.gif' }
- let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) }
let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
let(:temp_description) do
@@ -12,7 +13,7 @@ describe FileMover do
let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) }
let(:snippet) { create(:personal_snippet, description: temp_description) }
- subject { described_class.new(file_path, snippet).execute }
+ subject { described_class.new(temp_file_path, snippet).execute }
describe '#execute' do
before do
@@ -20,6 +21,8 @@ describe FileMover do
expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
+
+ stub_file_mover(temp_file_path)
end
context 'when move and field update successful' do
@@ -66,4 +69,30 @@ describe FileMover do
end
end
end
+
+ context 'security' do
+ context 'when relative path is involved' do
+ let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') }
+
+ it 'does not trigger move if path is outside designated directory' do
+ stub_file_mover('uploads/-/system/another_subdir_of_temp')
+ expect(FileUtils).not_to receive(:move)
+
+ subject
+
+ expect(snippet.reload.description).to eq(temp_description)
+ end
+ end
+
+ context 'when symlink is involved' do
+ it 'does not trigger move if path is outside designated directory' do
+ stub_file_mover(temp_file_path, stub_real_path: Pathname('/etc'))
+ expect(FileUtils).not_to receive(:move)
+
+ subject
+
+ expect(snippet.reload.description).to eq(temp_description)
+ end
+ end
+ end
end
diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb
new file mode 100644
index 00000000000..b9242ef931e
--- /dev/null
+++ b/spec/validators/sha_validator_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe ShaValidator do
+ let(:validator) { described_class.new(attributes: [:base_commit_sha]) }
+ let(:merge_diff) { build(:merge_request_diff) }
+
+ subject { validator.validate_each(merge_diff, :base_commit_sha, value) }
+
+ context 'with empty value' do
+ let(:value) { nil }
+
+ it 'does not add any error if value is empty' do
+ subject
+
+ expect(merge_diff.errors).to be_empty
+ end
+ end
+
+ context 'with valid sha' do
+ let(:value) { Digest::SHA1.hexdigest(SecureRandom.hex) }
+
+ it 'does not add any error if value is empty' do
+ subject
+
+ expect(merge_diff.errors).to be_empty
+ end
+ end
+
+ context 'with invalid sha' do
+ let(:value) { 'foo' }
+
+ it 'adds error to the record' do
+ expect(merge_diff.errors).to be_empty
+
+ subject
+
+ expect(merge_diff.errors).not_to be_empty
+ end
+ end
+end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
index 9d1efcabb80..cbb4199954a 100644
--- a/spec/views/layouts/_head.html.haml_spec.rb
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -62,6 +62,14 @@ describe 'layouts/_head' do
end
end
+ it 'adds selected syntax highlight stylesheet' do
+ allow_any_instance_of(PreferencesHelper).to receive(:user_color_scheme).and_return("solarised-light")
+
+ render
+
+ expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />')
+ end
+
def stub_helper_with_safe_string(method)
allow_any_instance_of(PageLayoutHelper).to receive(method)
.and_return(%q{foo" http-equiv="refresh}.html_safe)
diff --git a/spec/views/projects/commits/_commit.html.haml_spec.rb b/spec/views/projects/commits/_commit.html.haml_spec.rb
index 00547e433c4..6bf1b5fd2d0 100644
--- a/spec/views/projects/commits/_commit.html.haml_spec.rb
+++ b/spec/views/projects/commits/_commit.html.haml_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
describe 'projects/commits/_commit.html.haml' do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.repository.commit(ref) }
+
before do
allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings)
end
- context 'with a singed commit' do
- let(:project) { create(:project, :repository) }
- let(:repository) { project.repository }
+ context 'with a signed commit' do
let(:ref) { GpgHelpers::SIGNED_COMMIT_SHA }
- let(:commit) { repository.commit(ref) }
it 'does not display a loading spinner for GPG status' do
render partial: 'projects/commits/commit', locals: {
@@ -23,4 +23,55 @@ describe 'projects/commits/_commit.html.haml' do
end
end
end
+
+ context 'with ci status' do
+ let(:ref) { 'master' }
+ let(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+
+ project.add_developer(user)
+
+ create(
+ :ci_empty_pipeline,
+ ref: 'master',
+ sha: commit.id,
+ status: 'success',
+ project: project
+ )
+ end
+
+ context 'when pipelines are disabled' do
+ before do
+ allow(project).to receive(:builds_enabled?).and_return(false)
+ end
+
+ it 'does not display a ci status icon' do
+ render partial: 'projects/commits/commit', locals: {
+ project: project,
+ ref: ref,
+ commit: commit
+ }
+
+ expect(rendered).not_to have_css('.ci-status-link')
+ end
+ end
+
+ context 'when pipelines are enabled' do
+ before do
+ allow(project).to receive(:builds_enabled?).and_return(true)
+ end
+
+ it 'does display a ci status icon when pipelines are enabled' do
+ render partial: 'projects/commits/commit', locals: {
+ project: project,
+ ref: ref,
+ commit: commit
+ }
+
+ expect(rendered).to have_css('.ci-status-link')
+ end
+ end
+ end
end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index ff88efd0e31..1d9c6d36ad7 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -21,12 +21,24 @@ describe 'projects/issues/show' do
allow(issue).to receive(:closed?).and_return(true)
end
- it 'shows "Closed (moved)" if an issue has been moved' do
- allow(issue).to receive(:moved?).and_return(true)
+ context 'when the issue was moved' do
+ let(:new_issue) { create(:issue, project: project, author: user) }
- render
+ before do
+ issue.moved_to = new_issue
+ end
+
+ it 'shows "Closed (moved)" if an issue has been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ end
+
+ it 'links "moved" to the new issue the original issue was moved to' do
+ render
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
it 'shows "Closed" if an issue has not been moved' do
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index acd8da11d8d..ccb26849e67 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -26,5 +26,24 @@ describe BuildFinishedWorker do
.not_to raise_error
end
end
+
+ it 'schedules a ChatNotification job for a chat build' do
+ build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat))
+
+ expect(ChatNotificationWorker)
+ .to receive(:perform_async)
+ .with(build.id)
+
+ described_class.new.perform(build.id)
+ end
+
+ it 'does not schedule a ChatNotification job for a regular build' do
+ build = create(:ci_build, :success, pipeline: create(:ci_pipeline))
+
+ expect(ChatNotificationWorker)
+ .not_to receive(:perform_async)
+
+ described_class.new.perform(build.id)
+ end
end
end
diff --git a/spec/workers/chat_notification_worker_spec.rb b/spec/workers/chat_notification_worker_spec.rb
new file mode 100644
index 00000000000..91695674f5d
--- /dev/null
+++ b/spec/workers/chat_notification_worker_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ChatNotificationWorker do
+ let(:worker) { described_class.new }
+ let(:chat_build) do
+ create(:ci_build, pipeline: create(:ci_pipeline, source: :chat))
+ end
+
+ describe '#perform' do
+ it 'does nothing when the build no longer exists' do
+ expect(worker).not_to receive(:send_response)
+
+ worker.perform(-1)
+ end
+
+ it 'sends a response for an existing build' do
+ expect(worker)
+ .to receive(:send_response)
+ .with(an_instance_of(Ci::Build))
+
+ worker.perform(chat_build.id)
+ end
+
+ it 'reschedules the job if the trace sections could not be found' do
+ expect(worker)
+ .to receive(:send_response)
+ .and_raise(Gitlab::Chat::Output::MissingBuildSectionError)
+
+ expect(described_class)
+ .to receive(:perform_in)
+ .with(described_class::RESCHEDULE_INTERVAL, chat_build.id)
+
+ worker.perform(chat_build.id)
+ end
+ end
+
+ describe '#send_response' do
+ context 'when a responder could not be found' do
+ it 'does nothing' do
+ expect(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(chat_build)
+ .and_return(nil)
+
+ expect(worker.send_response(chat_build)).to be_nil
+ end
+ end
+
+ context 'when a responder could be found' do
+ let(:responder) { double(:responder) }
+
+ before do
+ allow(Gitlab::Chat::Responder)
+ .to receive(:responder_for)
+ .with(chat_build)
+ .and_return(responder)
+ end
+
+ it 'sends the response for a succeeded build' do
+ output = double(:output, to_s: 'this is the build output')
+
+ expect(chat_build)
+ .to receive(:success?)
+ .and_return(true)
+
+ expect(responder)
+ .to receive(:success)
+ .with(an_instance_of(String))
+
+ expect(Gitlab::Chat::Output)
+ .to receive(:new)
+ .with(chat_build)
+ .and_return(output)
+
+ worker.send_response(chat_build)
+ end
+
+ it 'sends the response for a failed build' do
+ expect(chat_build)
+ .to receive(:success?)
+ .and_return(false)
+
+ expect(responder).to receive(:failure)
+
+ worker.send_response(chat_build)
+ end
+ end
+ end
+end
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index 963237ceadf..f29e49f202a 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -18,7 +18,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do
context 'when merge request sha does not equal pipeline sha' do
before do
- merge_request.merge_request_diff.update(head_commit_sha: 'different_sha')
+ merge_request.merge_request_diff.update(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex))
end
it 'does not update head pipeline' do