summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-07-03 13:24:02 +0900
committerShinya Maeda <shinya@gitlab.com>2018-07-03 13:24:02 +0900
commitf930e94363e00e41fceb31f7d8497e5fb530dacd (patch)
treebf7129ec74644026bc7662f525ce77ab38acaae8 /spec
parent58a1a0b70c7df0947864d0be933faf0153b537ec (diff)
parent275fbf24b1810e2fbef92b6599d5372855b97b46 (diff)
downloadgitlab-ce-f930e94363e00e41fceb31f7d8497e5fb530dacd.tar.gz
Merge branch 'master' into build-chunks-on-object-storage
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/changelog_spec.rb11
-rw-r--r--spec/controllers/health_controller_spec.rb4
-rw-r--r--spec/controllers/metrics_controller_spec.rb7
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb34
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb189
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/dependencies/omniauth_saml_spec.rb22
-rw-r--r--spec/features/admin/admin_settings_spec.rb23
-rw-r--r--spec/features/groups/empty_states_spec.rb30
-rw-r--r--spec/features/groups/issues_spec.rb22
-rw-r--r--spec/features/groups/labels/index_spec.rb17
-rw-r--r--spec/features/groups/merge_requests_spec.rb17
-rw-r--r--spec/features/groups/milestone_spec.rb13
-rw-r--r--spec/features/ics/dashboard_issues_spec.rb37
-rw-r--r--spec/features/milestones/user_deletes_milestone_spec.rb1
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb13
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb2
-rw-r--r--spec/features/projects/graph_spec.rb20
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343091 -> 343136 bytes
-rw-r--r--spec/features/projects/issues/user_creates_issue_spec.rb3
-rw-r--r--spec/features/projects/milestones/new_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb4
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb4
-rw-r--r--spec/features/protected_branches_spec.rb47
-rw-r--r--spec/features/tags/master_creates_tag_spec.rb4
-rw-r--r--spec/features/tags/master_updates_tag_spec.rb4
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb27
-rw-r--r--spec/features/users/login_spec.rb35
-rw-r--r--spec/features/users/signup_spec.rb9
-rw-r--r--spec/finders/merge_requests_finder_spec.rb14
-rw-r--r--spec/finders/user_recent_events_finder_spec.rb45
-rw-r--r--spec/fixtures/authentication/saml_response.xml42
-rw-r--r--spec/fixtures/exported-project.gzbin2560 -> 0 bytes
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb5
-rw-r--r--spec/graphql/types/permission_types/base_permission_type_spec.rb47
-rw-r--r--spec/graphql/types/permission_types/merge_request_spec.rb13
-rw-r--r--spec/graphql/types/permission_types/merge_request_type_spec.rb5
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb18
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/helpers/application_helper_spec.rb2
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb2
-rw-r--r--spec/helpers/projects_helper_spec.rb9
-rw-r--r--spec/initializers/6_validations_spec.rb43
-rw-r--r--spec/javascripts/api_spec.js27
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js55
-rw-r--r--spec/javascripts/blob/3d_viewer/mesh_object_spec.js4
-rw-r--r--spec/javascripts/boards/issue_card_spec.js8
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/deploy_keys/components/key_spec.js2
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js96
-rw-r--r--spec/javascripts/diffs/components/diff_line_gutter_content_spec.js49
-rw-r--r--spec/javascripts/diffs/components/diff_line_note_form_spec.js19
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js66
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js195
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js30
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js8
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js8
-rw-r--r--spec/javascripts/filtered_search/filtered_search_token_keys_spec.js11
-rw-r--r--spec/javascripts/filtered_search/recent_searches_root_spec.js3
-rw-r--r--spec/javascripts/gl_field_errors_spec.js2
-rw-r--r--spec/javascripts/groups/components/app_spec.js6
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js2
-rw-r--r--spec/javascripts/helpers/init_vue_mr_page_helper.js14
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/form_spec.js13
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/message_field_spec.js1
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js15
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js106
-rw-r--r--spec/javascripts/ide/components/ide_spec.js14
-rw-r--r--spec/javascripts/ide/components/repo_commit_section_spec.js18
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js26
-rw-r--r--spec/javascripts/ide/helpers.js26
-rw-r--r--spec/javascripts/ide/lib/diff/controller_spec.js2
-rw-r--r--spec/javascripts/ide/mock_data.js1
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js276
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js267
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js155
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js234
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js14
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js30
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js92
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js8
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js54
-rw-r--r--spec/javascripts/merge_request_spec.js23
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js1
-rw-r--r--spec/javascripts/namespace_select_spec.js4
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js1
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js13
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js7
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js11
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js30
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js2
-rw-r--r--spec/javascripts/smart_interval_spec.js4
-rw-r--r--spec/javascripts/test_bundle.js1
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js84
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js2
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb9
-rw-r--r--spec/lib/gitaly/server_spec.rb34
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb8
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb51
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb41
-rw-r--r--spec/lib/gitlab/background_migration/delete_diff_files_spec.rb69
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb64
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb10
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb8
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb55
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb12
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb132
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb140
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb200
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/group_project_object_builder_spec.rb52
-rw-r--r--spec/lib/gitlab/import_export/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project.light.json8
-rw-r--r--spec/lib/gitlab/import_export/project.milestone-iid.json80
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb82
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb35
-rw-r--r--spec/lib/gitlab/repository_cache_adapter_spec.rb12
-rw-r--r--spec/lib/gitlab/shard_health_cache_spec.rb52
-rw-r--r--spec/lib/mattermost/session_spec.rb20
-rw-r--r--spec/migrations/cleanup_stages_position_migration_spec.rb67
-rw-r--r--spec/migrations/enqueue_delete_diff_files_workers_spec.rb48
-rw-r--r--spec/models/application_setting_spec.rb36
-rw-r--r--spec/models/ci/build_spec.rb6
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb26
-rw-r--r--spec/models/commit_spec.rb31
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb11
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb13
-rw-r--r--spec/models/concerns/sortable_spec.rb18
-rw-r--r--spec/models/merge_request_diff_spec.rb39
-rw-r--r--spec/models/merge_request_spec.rb103
-rw-r--r--spec/models/namespace_spec.rb13
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/repository_spec.rb86
-rw-r--r--spec/requests/api/boards_spec.rb1
-rw-r--r--spec/requests/api/branches_spec.rb29
-rw-r--r--spec/requests/api/files_spec.rb85
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb70
-rw-r--r--spec/requests/api/graphql/project_query_spec.rb44
-rw-r--r--spec/requests/api/issues_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb15
-rw-r--r--spec/requests/api/project_snippets_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb32
-rw-r--r--spec/requests/api/repositories_spec.rb25
-rw-r--r--spec/requests/api/snippets_spec.rb2
-rw-r--r--spec/requests/oauth_tokens_spec.rb55
-rw-r--r--spec/requests/openid_connect_spec.rb110
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb17
-rw-r--r--spec/services/issues/move_service_spec.rb26
-rw-r--r--spec/services/keys/last_used_service_spec.rb4
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb59
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb39
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb25
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb64
-rw-r--r--spec/services/projects/batch_open_issues_count_service_spec.rb54
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb35
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb305
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb4
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb10
-rw-r--r--spec/services/web_hook_service_spec.rb30
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb36
-rw-r--r--spec/support/helpers/login_helpers.rb36
-rw-r--r--spec/support/helpers/stub_object_storage.rb7
-rw-r--r--spec/support/matchers/graphql_matchers.rb29
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/features/protected_branches_access_control_ce.rb36
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb18
-rw-r--r--spec/support/shared_examples/requests/graphql_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/throttled_touch.rb4
-rw-r--r--spec/support/shared_examples/uploaders/object_storage_shared_examples.rb8
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb10
-rw-r--r--spec/workers/delete_diff_files_worker_spec.rb41
-rw-r--r--spec/workers/delete_user_worker_spec.rb10
-rw-r--r--spec/workers/project_cache_worker_spec.rb86
-rw-r--r--spec/workers/project_migrate_hashed_storage_worker_spec.rb60
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb38
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb29
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb36
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb57
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb38
190 files changed, 4237 insertions, 2227 deletions
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index fc1bf67d7b9..f278043028f 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -56,11 +56,11 @@ describe 'bin/changelog' do
it 'parses -h' do
expect do
expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout
- end.to raise_error(SystemExit)
+ end.to raise_error(ChangelogHelpers::Done)
end
it 'assigns title' do
- options = described_class.parse(%W[foo -m 1 bar\n -u baz\r\n --amend])
+ options = described_class.parse(%W[foo -m 1 bar\n baz\r\n --amend])
expect(options.title).to eq 'foo bar baz'
end
@@ -82,9 +82,10 @@ describe 'bin/changelog' do
it 'shows error message and exits the program' do
allow($stdin).to receive(:getc).and_return(type)
expect do
- expect do
- expect { described_class.read_type }.to raise_error(SystemExit)
- end.to output("Invalid category index, please select an index between 1 and 8\n").to_stderr
+ expect { described_class.read_type }.to raise_error(
+ ChangelogHelpers::Abort,
+ 'Invalid category index, please select an index between 1 and 8'
+ )
end.to output.to_stdout
end
end
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 542eddc2d16..d800ad7c187 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -69,8 +69,7 @@ describe HealthController do
expect(json_response['cache_check']['status']).to eq('ok')
expect(json_response['queues_check']['status']).to eq('ok')
expect(json_response['shared_state_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['labels']['shard']).to eq('default')
+ expect(json_response['gitaly_check']['status']).to eq('ok')
end
end
@@ -122,7 +121,6 @@ describe HealthController do
expect(json_response['cache_check']['status']).to eq('ok')
expect(json_response['queues_check']['status']).to eq('ok')
expect(json_response['shared_state_check']['status']).to eq('ok')
- expect(json_response['fs_shards_check']['status']).to eq('ok')
end
end
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index 9e8a37171ec..7376841fac8 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -59,6 +59,13 @@ describe MetricsController do
expect(response.body).to match(/^redis_shared_state_ping_latency_seconds [0-9\.]+$/)
end
+ it 'returns Gitaly metrics' do
+ get :index
+
+ expect(response.body).to match(/^gitaly_health_check_success{shard="default"} 1$/)
+ expect(response.body).to match(/^gitaly_health_check_latency_seconds{shard="default"} [0-9\.]+$/)
+ end
+
context 'prometheus metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false)
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 149b690ff70..8c10ea53a7a 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -2,19 +2,12 @@ require 'spec_helper'
describe Oauth::AuthorizationsController do
let(:user) { create(:user) }
-
- let(:doorkeeper) do
- Doorkeeper::Application.create(
- name: "MyApp",
- redirect_uri: 'http://example.com',
- scopes: "")
- end
-
+ let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
let(:params) do
{
response_type: "code",
- client_id: doorkeeper.uid,
- redirect_uri: doorkeeper.redirect_uri,
+ client_id: application.uid,
+ redirect_uri: application.redirect_uri,
state: 'state'
}
end
@@ -44,7 +37,7 @@ describe Oauth::AuthorizationsController do
end
it 'deletes session.user_return_to and redirects when skip authorization' do
- doorkeeper.update(trusted: true)
+ application.update(trusted: true)
request.session['user_return_to'] = 'http://example.com'
get :new, params
@@ -52,6 +45,25 @@ describe Oauth::AuthorizationsController do
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(302)
end
+
+ context 'when there is already an access token for the application' do
+ context 'when the request scope matches any of the created token scopes' do
+ before do
+ scopes = Doorkeeper::OAuth::Scopes.from_string('api')
+
+ allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
+
+ create :oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes
+ end
+
+ it 'authorizes the request and redirects' do
+ get :new, params
+
+ expect(request.session['user_return_to']).to be_nil
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
end
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 5f0e8c5eca9..b23f183fec8 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -1,127 +1,162 @@
require 'spec_helper'
-describe OmniauthCallbacksController do
+describe OmniauthCallbacksController, type: :controller do
include LoginHelpers
- let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
-
- before do
- mock_auth_hash(provider.to_s, extern_uid, user.email)
- stub_omniauth_provider(provider, context: request)
- end
-
- context 'when the user is on the last sign in attempt' do
- let(:extern_uid) { 'my-uid' }
+ describe 'omniauth' do
+ let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- user.update(failed_attempts: User.maximum_attempts.pred)
- subject.response = ActionDispatch::Response.new
+ mock_auth_hash(provider.to_s, extern_uid, user.email)
+ stub_omniauth_provider(provider, context: request)
end
- context 'when using a form based provider' do
- let(:provider) { :ldap }
-
- it 'locks the user when sign in fails' do
- allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
-
- subject.send(:failure)
+ context 'when the user is on the last sign in attempt' do
+ let(:extern_uid) { 'my-uid' }
- expect(user.reload).to be_access_locked
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ subject.response = ActionDispatch::Response.new
end
- end
- context 'when using a button based provider' do
- let(:provider) { :github }
+ context 'when using a form based provider' do
+ let(:provider) { :ldap }
- it 'does not lock the user when sign in fails' do
- request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
+ it 'locks the user when sign in fails' do
+ allow(subject).to receive(:params).and_return(ActionController::Parameters.new(username: user.username))
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::LDAP.new(nil)
- subject.send(:failure)
+ subject.send(:failure)
- expect(user.reload).not_to be_access_locked
+ expect(user.reload).to be_access_locked
+ end
end
- end
- end
- context 'strategies' do
- context 'github' do
- let(:extern_uid) { 'my-uid' }
- let(:provider) { :github }
+ context 'when using a button based provider' do
+ let(:provider) { :github }
- it 'allows sign in' do
- post provider
+ it 'does not lock the user when sign in fails' do
+ request.env['omniauth.error.strategy'] = OmniAuth::Strategies::GitHub.new(nil)
- expect(request.env['warden']).to be_authenticated
- end
-
- shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ subject.send(:failure)
- before do
- stub_omniauth_setting(block_auto_created_users: false)
+ expect(user.reload).not_to be_access_locked
end
end
+ end
- context 'sign up' do
- include_context 'sign_up'
+ context 'strategies' do
+ context 'github' do
+ let(:extern_uid) { 'my-uid' }
+ let(:provider) { :github }
- it 'is allowed' do
+ it 'allows sign in' do
post provider
expect(request.env['warden']).to be_authenticated
end
- end
-
- context 'when OAuth is disabled' do
- before do
- stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- settings = Gitlab::CurrentSettings.current_application_settings
- settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
- end
- it 'prevents login via POST' do
- post provider
+ shared_context 'sign_up' do
+ let(:user) { double(email: 'new@example.com') }
- expect(request.env['warden']).not_to be_authenticated
+ before do
+ stub_omniauth_setting(block_auto_created_users: false)
+ end
end
- it 'shows warning when attempting login' do
- post provider
-
- expect(response).to redirect_to new_user_session_path
- expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
- end
+ context 'sign up' do
+ include_context 'sign_up'
- it 'allows linking the disabled provider' do
- user.identities.destroy_all
- sign_in(user)
+ it 'is allowed' do
+ post provider
- expect { post provider }.to change { user.reload.identities.count }.by(1)
+ expect(request.env['warden']).to be_authenticated
+ end
end
- context 'sign up' do
- include_context 'sign_up'
+ context 'when OAuth is disabled' do
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ settings = Gitlab::CurrentSettings.current_application_settings
+ settings.update(disabled_oauth_sign_in_sources: [provider.to_s])
+ end
- it 'is prevented' do
+ it 'prevents login via POST' do
post provider
expect(request.env['warden']).not_to be_authenticated
end
+
+ it 'shows warning when attempting login' do
+ post provider
+
+ expect(response).to redirect_to new_user_session_path
+ expect(flash[:alert]).to eq('Signing in using GitHub has been disabled')
+ end
+
+ it 'allows linking the disabled provider' do
+ user.identities.destroy_all
+ sign_in(user)
+
+ expect { post provider }.to change { user.reload.identities.count }.by(1)
+ end
+
+ context 'sign up' do
+ include_context 'sign_up'
+
+ it 'is prevented' do
+ post provider
+
+ expect(request.env['warden']).not_to be_authenticated
+ end
+ end
+ end
+ end
+
+ context 'auth0' do
+ let(:extern_uid) { '' }
+ let(:provider) { :auth0 }
+
+ it 'does not allow sign in without extern_uid' do
+ post 'auth0'
+
+ expect(request.env['warden']).not_to be_authenticated
+ expect(response.status).to eq(302)
+ expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
+ end
+
+ describe '#saml' do
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml') }
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ 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)
+ request.env["devise.mapping"] = Devise.mappings[:user]
+ request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
+ post :saml, params: { SAMLResponse: mock_saml_response }
+ end
- context 'auth0' do
- let(:extern_uid) { '' }
- let(:provider) { :auth0 }
+ context 'when worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN')
+ end
- it 'does not allow sign in without extern_uid' do
- post 'auth0'
+ it 'expects user to be signed_in' do
+ expect(request.env['warden']).to be_authenticated
+ end
+ end
+ context 'when not worth two factors' do
+ it 'expects user to provide second factor' do
+ expect(response).to render_template('devise/sessions/two_factor')
expect(request.env['warden']).not_to be_authenticated
- expect(response.status).to eq(302)
- expect(controller).to set_flash[:alert].to('Wrong extern UID provided. Make sure Auth0 is configured correctly.')
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 02b30f9bc6d..b1d83246238 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -124,7 +124,7 @@ describe Projects::MilestonesController do
it 'shows group milestone' do
post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
- expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.")
+ expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\"><u>group milestone</u></a>.")
expect(response).to redirect_to(project_milestones_path(project))
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 90e698925b6..27f04be3fdf 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -329,7 +329,7 @@ describe ProjectsController do
expect { update_project path: 'renamed_path' }
.not_to change { project.reload.path }
- expect(controller).to set_flash[:alert].to(/container registry tags/)
+ expect(controller).to set_flash.now[:alert].to(/container registry tags/)
expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/dependencies/omniauth_saml_spec.rb b/spec/dependencies/omniauth_saml_spec.rb
new file mode 100644
index 00000000000..ccc604dc230
--- /dev/null
+++ b/spec/dependencies/omniauth_saml_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+require 'omniauth/strategies/saml'
+
+describe 'processing of SAMLResponse in dependencies' do
+ let(:mock_saml_response) { File.read('spec/fixtures/authentication/saml_response.xml') }
+ let(:saml_strategy) { OmniAuth::Strategies::SAML.new({}) }
+ let(:session_mock) { {} }
+ let(:settings) { OpenStruct.new({ soft: false, idp_cert_fingerprint: 'something' }) }
+ let(:auth_hash) { Gitlab::Auth::Saml::AuthHash.new(saml_strategy) }
+
+ subject { auth_hash.authn_context }
+
+ before do
+ allow(saml_strategy).to receive(:session).and_return(session_mock)
+ allow_any_instance_of(OneLogin::RubySaml::Response).to receive(:is_valid?).and_return(true)
+ saml_strategy.send(:handle_response, mock_saml_response, {}, settings ) { }
+ end
+
+ it 'can extract AuthnContextClassRef from SAMLResponse param' do
+ is_expected.to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e7aca94db66..f3ab4ff771a 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -124,6 +124,29 @@ feature 'Admin updates settings' do
expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2')
end
+ scenario 'Oauth providers do not raise validation errors when saving unrelated changes' do
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty
+
+ page.within('.as-signin') do
+ uncheck 'Google'
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+
+ # Remove google_oauth2 from the Omniauth strategies
+ allow(Devise).to receive(:omniauth_providers).and_return([])
+
+ # Save an unrelated setting
+ page.within('.as-ci-cd') do
+ click_button 'Save changes'
+ end
+
+ expect(page).to have_content "Application settings saved successfully"
+ expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2')
+ end
+
scenario 'Change Help page' do
page.within('.as-help-page') do
fill_in 'Help page text', with: 'Example text'
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 04217fec06c..5828d833ae9 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -59,6 +59,18 @@ feature 'Group empty states' do
end
end
+ shared_examples "no projects" do
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+
+ it "does not show a new #{issuable_name} button" do
+ within '.empty-state' do
+ expect(page).not_to have_link("create #{issuable_name}")
+ end
+ end
+ end
+
context 'group without a project' do
context 'group has a subgroup', :nested_groups do
let(:subgroup) { create(:group, parent: group) }
@@ -92,16 +104,18 @@ feature 'Group empty states' do
visit path
end
- it 'displays an empty state' do
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "no projects"
+ end
+ end
- it "shows a new #{issuable_name} button" do
- within '.empty-state' do
- expect(page).not_to have_link("create #{issuable_name}")
- end
- end
+ context 'group has only a project with issues disabled' do
+ let(:project_with_issues_disabled) { create(:empty_project, :issues_disabled, group: group) }
+
+ before do
+ visit path
end
+
+ it_behaves_like "no projects"
end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 111a24c0d94..e131ded3688 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -5,6 +5,7 @@ feature 'Group issues page' do
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group)}
+ let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) }
let(:path) { issues_group_path(group) }
context 'with shared examples' do
@@ -76,4 +77,25 @@ feature 'Group issues page' do
end
end
end
+
+ context 'projects with issues disabled' do
+ describe 'issue dropdown' do
+ let(:user_in_group) { create(:group_member, :master, user: create(:user), group: group ).user }
+
+ before do
+ [project, project_with_issues_disabled].each { |project| project.add_master(user_in_group) }
+ sign_in(user_in_group)
+ visit issues_group_path(group)
+ end
+
+ it 'shows projects only with issues feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb
new file mode 100644
index 00000000000..6c1b43a9013
--- /dev/null
+++ b/spec/features/groups/labels/index_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+feature 'Group labels' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let!(:label) { create(:group_label, group: group) }
+
+ background do
+ group.add_owner(user)
+ sign_in(user)
+ visit group_labels_path(group)
+ end
+
+ scenario 'label has edit button', :js do
+ expect(page).to have_selector('.label-action.edit')
+ end
+end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 672ae785c2d..921a447f6ee 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -56,4 +56,21 @@ feature 'Group merge requests page' do
expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
end
end
+
+ describe 'new merge request dropdown' do
+ let(:project_with_merge_requests_disabled) { create(:project, :merge_requests_disabled, group: group) }
+
+ before do
+ visit path
+ end
+
+ it 'shows projects only with merge requests feature enabled', :js do
+ find('.new-project-item-link').click
+
+ page.within('.select2-results') do
+ expect(page).to have_content(project.name_with_namespace)
+ expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace)
+ end
+ end
+ end
end
diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb
index 20337f1d3b0..2108d763028 100644
--- a/spec/features/groups/milestone_spec.rb
+++ b/spec/features/groups/milestone_spec.rb
@@ -107,19 +107,6 @@ feature 'Group milestones' do
expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1)
end
- it 'updates milestone' do
- page.within(".milestones #milestone_#{active_group_milestone.id}") do
- click_link('Edit')
- end
-
- page.within('.milestone-form') do
- fill_in 'milestone_title', with: 'new title'
- click_button('Update milestone')
- end
-
- expect(find('#content-body h2')).to have_content('new title')
- end
-
it 'shows milestone detail and supports its edit' do
page.within(".milestones #milestone_#{active_group_milestone.id}") do
click_link(active_group_milestone.title)
diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb
index 90d02f7e40f..a4d05c25a90 100644
--- a/spec/features/ics/dashboard_issues_spec.rb
+++ b/spec/features/ics/dashboard_issues_spec.rb
@@ -5,6 +5,7 @@ describe 'Dashboard Issues Calendar Feed' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:project) { create(:project) }
+ let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') }
before do
project.add_master(user)
@@ -14,7 +15,9 @@ describe 'Dashboard Issues Calendar Feed' do
context 'with no referer' do
it 'renders calendar feed' do
sign_in user
- visit issues_dashboard_path(:ics)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -25,19 +28,37 @@ describe 'Dashboard Issues Calendar Feed' do
it 'renders calendar feed as text/plain' do
sign_in user
page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url))
- visit issues_dashboard_path(:ics)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date')
expect(response_headers['Content-Type']).to have_content('text/plain')
expect(body).to have_text('BEGIN:VCALENDAR')
end
end
+
+ context 'when filtered by milestone' do
+ it 'renders calendar feed' do
+ sign_in user
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ milestone_title: milestone.title)
+
+ expect(response_headers['Content-Type']).to have_content('text/calendar')
+ expect(body).to have_text('BEGIN:VCALENDAR')
+ end
+ end
end
context 'when authenticated via personal access token' do
it 'renders calendar feed' do
personal_access_token = create(:personal_access_token, user: user)
- visit issues_dashboard_path(:ics, private_token: personal_access_token.token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ private_token: personal_access_token.token)
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -46,7 +67,10 @@ describe 'Dashboard Issues Calendar Feed' do
context 'when authenticated via feed token' do
it 'renders calendar feed' do
- visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
expect(response_headers['Content-Type']).to have_content('text/calendar')
expect(body).to have_text('BEGIN:VCALENDAR')
@@ -60,7 +84,10 @@ describe 'Dashboard Issues Calendar Feed' do
end
it 'renders issue fields' do
- visit issues_dashboard_path(:ics, feed_token: user.feed_token)
+ visit issues_dashboard_path(:ics,
+ due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
+ sort: 'closest_future_date',
+ feed_token: user.feed_token)
expect(body).to have_text("SUMMARY:test title (in #{project.full_path})")
# line length for ics is 75 chars
diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb
index 414702daba4..9d4a68239d3 100644
--- a/spec/features/milestones/user_deletes_milestone_spec.rb
+++ b/spec/features/milestones/user_deletes_milestone_spec.rb
@@ -13,6 +13,7 @@ describe "User deletes milestone", :js do
end
it "deletes milestone" do
+ click_link(milestone.title)
click_button("Delete")
click_button("Delete milestone")
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index c85b82b2090..3db384e5b65 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -157,6 +157,19 @@ feature 'Gcp Cluster', :js do
end
end
+ context 'when a user cannot edit the environment scope' do
+ before do
+ visit project_clusters_path(project)
+
+ click_link 'Add Kubernetes cluster'
+ click_link 'Add an existing Kubernetes cluster'
+ end
+
+ it 'user does not see the "Environment scope" field' do
+ expect(page).not_to have_css('#cluster_environment_scope')
+ end
+ end
+
context 'when user has not dismissed GCP signup offer' do
before do
visit project_clusters_path(project)
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
index 6397df086a7..53866c32c69 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -62,7 +62,7 @@ describe "User adds a comment on a commit", :js do
click_diff_line(sample_commit.line_code)
expect(page).to have_css(".js-temp-notes-holder form.new-note")
- .and have_css(".js-close-discussion-note-form", text: "Cancel")
+ .and have_css(".js-close-discussion-note-form", text: "Discard draft")
# The `Cancel` button closes the current form. The page should not have any open forms after that.
find(".js-close-discussion-note-form").click
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 57172610aed..335174b7729 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe 'Project Graph', :js do
let(:user) { create :user }
let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:branch_name) { 'master' }
before do
project.add_master(user)
@@ -12,7 +13,7 @@ describe 'Project Graph', :js do
shared_examples 'page should have commits graphs' do
it 'renders commits' do
- expect(page).to have_content('Commit statistics for master')
+ expect(page).to have_content("Commit statistics for #{branch_name}")
expect(page).to have_content('Commits per day of month')
end
end
@@ -57,6 +58,23 @@ describe 'Project Graph', :js do
it_behaves_like 'page should have languages graphs'
end
+ context 'chart graph with HTML escaped branch name' do
+ let(:branch_name) { '<h1>evil</h1>' }
+
+ before do
+ project.repository.create_branch(branch_name, 'master')
+
+ visit charts_project_graph_path(project, branch_name)
+ end
+
+ it_behaves_like 'page should have commits graphs'
+
+ it 'HTML escapes branch name' do
+ expect(page.body).to include("Commit statistics for <strong>#{ERB::Util.html_escape(branch_name)}</strong>")
+ expect(page.body).not_to include(branch_name)
+ end
+ end
+
context 'when CI enabled' do
before do
project.enable_ci
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 72ab2d71f35..ceba4dfec57 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/issues/user_creates_issue_spec.rb b/spec/features/projects/issues/user_creates_issue_spec.rb
index e76f7c5589d..5e8662100c5 100644
--- a/spec/features/projects/issues/user_creates_issue_spec.rb
+++ b/spec/features/projects/issues/user_creates_issue_spec.rb
@@ -17,6 +17,9 @@ describe "User creates issue" do
expect(page).to have_no_content("Assign to")
.and have_no_content("Labels")
.and have_no_content("Milestone")
+
+ expect(page.find('#issue_title')['placeholder']).to eq 'Title'
+ expect(page.find('#issue_description')['placeholder']).to eq 'Write a comment or drag your files here…'
end
issue_title = "500 error on profile"
diff --git a/spec/features/projects/milestones/new_spec.rb b/spec/features/projects/milestones/new_spec.rb
index f7900210fe6..6595bff549b 100644
--- a/spec/features/projects/milestones/new_spec.rb
+++ b/spec/features/projects/milestones/new_spec.rb
@@ -9,9 +9,9 @@ feature 'Creating a new project milestone', :js do
visit new_project_milestone_path(project)
end
- it 'description has autocomplete' do
+ it 'description has emoji autocomplete' do
find('#milestone_description').native.send_keys('')
- fill_in 'milestone_description', with: '@'
+ fill_in 'milestone_description', with: ':'
expect(page).to have_selector('.atwho-view')
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 706894f4b32..733e6c89de7 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -242,7 +242,7 @@ describe "User creates wiki page" do
end
end
- it "shows the autocompletion dropdown" do
+ it "shows the emoji autocompletion dropdown" do
click_link("New page")
page.within("#modal-new-wiki") do
@@ -254,7 +254,7 @@ describe "User creates wiki page" do
page.within(".wiki-form") do
find("#wiki_content").native.send_keys("")
- fill_in(:wiki_content, with: "@")
+ fill_in(:wiki_content, with: ":")
end
expect(page).to have_selector(".atwho-view")
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 272dac127dd..2ccbc15b6da 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -96,11 +96,11 @@ describe 'User updates wiki page' do
expect(find('textarea#wiki_content').value).to eq('')
end
- it 'shows the autocompletion dropdown', :js do
+ it 'shows the emoji autocompletion dropdown', :js do
click_link('Edit')
find('#wiki_content').native.send_keys('')
- fill_in(:wiki_content, with: '@')
+ fill_in(:wiki_content, with: ':')
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 4c0f9971425..39bd4af6cd0 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -60,33 +60,6 @@ feature 'Protected Branches', :js do
expect(page).to have_content('No branches to show')
end
end
-
- describe "Saved defaults" do
- it "keeps the allowed to merge and push dropdowns defaults based on the previous selection" do
- visit project_protected_branches_path(project)
- form = '.js-new-protected-branch'
-
- within form do
- find(".js-allowed-to-merge").click
- wait_for_requests
- click_link 'No one'
- find(".js-allowed-to-push").click
- wait_for_requests
- click_link 'Developers + Maintainers'
- end
-
- visit project_protected_branches_path(project)
-
- within form do
- page.within(".js-allowed-to-merge") do
- expect(page.find(".dropdown-toggle-text")).to have_content("No one")
- end
- page.within(".js-allowed-to-push") do
- expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Maintainers")
- end
- end
- end
- end
end
context 'logged in as admin' do
@@ -97,6 +70,7 @@ feature 'Protected Branches', :js do
describe "explicit protected branches" do
it "allows creating explicit protected branches" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -110,6 +84,7 @@ feature 'Protected Branches', :js do
project.repository.add_branch(admin, 'some-branch', commit.id)
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -118,6 +93,7 @@ feature 'Protected Branches', :js do
it "displays an error message if the named branch does not exist" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('some-branch')
click_on "Protect"
@@ -128,6 +104,7 @@ feature 'Protected Branches', :js do
describe "wildcard protected branches" do
it "allows creating protected branches with a wildcard" do
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('*-stable')
click_on "Protect"
@@ -141,6 +118,7 @@ feature 'Protected Branches', :js do
project.repository.add_branch(admin, 'staging-stable', 'master')
visit project_protected_branches_path(project)
+ set_defaults
set_protected_branch_name('*-stable')
click_on "Protect"
@@ -157,6 +135,7 @@ feature 'Protected Branches', :js do
visit project_protected_branches_path(project)
set_protected_branch_name('*-stable')
+ set_defaults
click_on "Protect"
visit project_protected_branches_path(project)
@@ -180,4 +159,18 @@ feature 'Protected Branches', :js do
find(".dropdown-input-field").set(branch_name)
click_on("Create wildcard #{branch_name}")
end
+
+ def set_defaults
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+ end
end
diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb
index 8a8f6933fa5..6701f575a23 100644
--- a/spec/features/tags/master_creates_tag_spec.rb
+++ b/spec/features/tags/master_creates_tag_spec.rb
@@ -75,9 +75,9 @@ feature 'Master creates tag' do
visit new_project_tag_path(project)
end
- it 'description has autocomplete', :js do
+ it 'description has emoji autocomplete', :js do
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/tags/master_updates_tag_spec.rb b/spec/features/tags/master_updates_tag_spec.rb
index 1c370a99b13..26f51bee887 100644
--- a/spec/features/tags/master_updates_tag_spec.rb
+++ b/spec/features/tags/master_updates_tag_spec.rb
@@ -25,13 +25,13 @@ feature 'Master updates tag' do
expect(page).to have_content 'Awesome release notes'
end
- scenario 'description has autocomplete', :js do
+ scenario 'description has emoji autocomplete', :js do
page.within(first('.content-list .controls')) do
click_link 'Edit release notes'
end
find('#release_description').native.send_keys('')
- fill_in 'release_description', with: '@'
+ fill_in 'release_description', with: ':'
expect(page).to have_selector('.atwho-view')
end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 52003bb0859..766bb4f09cd 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -1,17 +1,16 @@
require 'rails_helper'
feature 'User uploads avatar to profile' do
- scenario 'they see their new avatar' do
- user = create(:user)
- sign_in(user)
+ let!(:user) { create(:user) }
+ let(:avatar_file_path) { Rails.root.join('spec', 'fixtures', 'dk.png') }
+ before do
+ sign_in user
visit profile_path
- attach_file(
- 'user_avatar',
- Rails.root.join('spec', 'fixtures', 'dk.png'),
- visible: false
- )
+ end
+ scenario 'they see their new avatar on their profile' do
+ attach_file('user_avatar', avatar_file_path, visible: false)
click_button 'Update profile settings'
visit user_path(user)
@@ -21,4 +20,16 @@ feature 'User uploads avatar to profile' do
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
end
+
+ scenario 'their new avatar is immediately visible in the header', :js do
+ find('.js-user-avatar-input', visible: false).set(avatar_file_path)
+
+ click_button 'Set new profile picture'
+ click_button 'Update profile settings'
+
+ wait_for_all_requests
+
+ data_uri = find('.avatar-image .avatar')['src']
+ expect(page.find('.header-user-avatar')['src']).to eq data_uri
+ end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 1f8d31a5c88..24a2c89f50b 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -177,14 +177,35 @@ feature 'Login' do
end
context 'logging in via OAuth' do
- it 'shows 2FA prompt after OAuth login' do
- stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
- user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
- gitlab_sign_in_via('saml', user, 'my-uid')
+ let(:user) { create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')}
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ end
- expect(page).to have_content('Two-Factor Authentication')
- enter_code(user.current_otp)
- expect(current_path).to eq root_path
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
+ providers: [mock_saml_config_with_upstream_two_factor_authn_contexts])
+ gitlab_sign_in_via('saml', user, 'my-uid', mock_saml_response)
+ end
+
+ context 'when authn_context is worth two factors' do
+ let(:mock_saml_response) do
+ File.read('spec/fixtures/authentication/saml_response.xml')
+ .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ end
+
+ it 'signs user in without prompting for second factor' do
+ expect(page).not_to have_content('Two-Factor Authentication')
+ expect(current_path).to eq root_path
+ end
+ end
+
+ context 'when authn_context is not worth two factors' do
+ it 'shows 2FA prompt after OAuth login' do
+ expect(page).to have_content('Two-Factor Authentication')
+ enter_code(user.current_otp)
+ expect(current_path).to eq root_path
+ end
end
end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index b51ca5d130b..bfe11ddf673 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -40,6 +40,15 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
+
+ it 'shows an error message on submit if the username contains special characters' do
+ fill_in 'new_user_username', with: 'new$user!username'
+ wait_for_requests
+
+ click_button "Register"
+
+ expect(page).to have_content("Please create a username with only alphanumeric characters.")
+ end
end
context 'with no errors' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index c8a43ddf410..669ec602f11 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -19,7 +19,7 @@ describe MergeRequestsFinder do
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
@@ -35,7 +35,7 @@ describe MergeRequestsFinder do
it 'filters by scope' do
params = { scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(4)
+ expect(merge_requests.size).to eq(3)
end
it 'filters by project' do
@@ -90,6 +90,14 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request2)
end
+ it 'filters by state' do
+ params = { state: 'locked' }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
+
context 'filtering by group milestone' do
let!(:group) { create(:group, :public) }
let(:group_milestone) { create(:milestone, group: group) }
@@ -199,7 +207,7 @@ describe MergeRequestsFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(4)
+ expect(finder.row_count).to eq(3)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 3ca0f7c3c89..da043f94021 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -1,31 +1,50 @@
require 'spec_helper'
describe UserRecentEventsFinder do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:project_owner) { project.creator }
- let!(:event) { create(:event, project: project, author: project_owner) }
+ let(:current_user) { create(:user) }
+ let(:project_owner) { create(:user) }
+ let(:private_project) { create(:project, :private, creator: project_owner) }
+ let(:internal_project) { create(:project, :internal, creator: project_owner) }
+ let(:public_project) { create(:project, :public, creator: project_owner) }
+ let!(:private_event) { create(:event, project: private_project, author: project_owner) }
+ let!(:internal_event) { create(:event, project: internal_project, author: project_owner) }
+ let!(:public_event) { create(:event, project: public_project, author: project_owner) }
- subject(:finder) { described_class.new(user, project_owner) }
+ subject(:finder) { described_class.new(current_user, project_owner) }
describe '#execute' do
- it 'does not include the event when a user does not have access to the project' do
- expect(finder.execute).to be_empty
+ context 'current user does not have access to projects' do
+ it 'returns public and internal events' do
+ records = finder.execute
+
+ expect(records).to include(public_event, internal_event)
+ expect(records).not_to include(private_event)
+ end
end
- context 'when the user has access to a project' do
+ context 'when current user has access to the projects' do
before do
- project.add_developer(user)
+ private_project.add_developer(current_user)
+ internal_project.add_developer(current_user)
+ public_project.add_developer(current_user)
end
- it 'includes the event' do
- expect(finder.execute).to include(event)
+ it 'returns all the events' do
+ expect(finder.execute).to include(private_event, internal_event, public_event)
end
- it 'does not include the event if the user cannot read cross project' do
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+ it 'does not include the events if the user cannot read cross project' do
+ expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
expect(finder.execute).to be_empty
end
end
+
+ context 'when current user is anonymous' do
+ let(:current_user) { nil }
+
+ it 'returns public events only' do
+ expect(finder.execute).to eq([public_event])
+ end
+ end
end
end
diff --git a/spec/fixtures/authentication/saml_response.xml b/spec/fixtures/authentication/saml_response.xml
new file mode 100644
index 00000000000..ac7b662be22
--- /dev/null
+++ b/spec/fixtures/authentication/saml_response.xml
@@ -0,0 +1,42 @@
+<?xml version='1.0'?>
+<samlp:Response xmlns:samlp='urn:oasis:names:tc:SAML:2.0:protocol' xmlns:saml='urn:oasis:names:tc:SAML:2.0:assertion' ID='pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a' Version='2.0' IssueInstant='2014-07-17T01:01:48Z' Destination='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><ds:Signature xmlns:ds='http://www.w3.org/2000/09/xmldsig#'>
+ <ds:SignedInfo><ds:CanonicalizationMethod Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/>
+ <ds:SignatureMethod Algorithm='http://www.w3.org/2000/09/xmldsig#rsa-sha1'/>
+ <ds:Reference URI='#pfxb9b71715-2202-9a51-8ae5-689d5b9dd25a'><ds:Transforms><ds:Transform Algorithm='http://www.w3.org/2000/09/xmldsig#enveloped-signature'/><ds:Transform Algorithm='http://www.w3.org/2001/10/xml-exc-c14n#'/></ds:Transforms><ds:DigestMethod Algorithm='http://www.w3.org/2000/09/xmldsig#sha1'/><ds:DigestValue>z0Y25hsUHVJJnYhgB5LzPVjqbgM=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>NSdsZopzNX4kJETipLNbU+7dG4GPTj5e40iSBaUeUMc1UUSX4UCe9Qx6R9ADEkEQgNekgYaCFOuY90kLNh9Ky0Czq8gd4w7ykQJEVJ7VF7LakmG8dPedHAKyAMAuZ8y3mNGye31vtR9frYaznCVoxB3eAi9rbVOXkQtdOTRMHec=</ds:SignatureValue>
+ <ds:KeyInfo><ds:X509Data><ds:X509Certificate>MIICajCCAdOgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBSMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRcwFQYDVQQDDA5zcC5leGFtcGxlLmNvbTAeFw0xNDA3MTcxNDEyNTZaFw0xNTA3MTcxNDEyNTZaMFIxCzAJBgNVBAYTAnVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxPbmVsb2dpbiBJbmMxFzAVBgNVBAMMDnNwLmV4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZx+ON4IUoIWxgukTb1tOiX3bMYzYQiwWPUNMp+Fq82xoNogso2bykZG0yiJm5o8zv/sd6pGouayMgkx/2FSOdc36T0jGbCHuRSbtia0PEzNIRtmViMrt3AeoWBidRXmZsxCNLwgIV6dn2WpuE5Az0bHgpZnQxTKFek0BMKU/d8wIDAQABo1AwTjAdBgNVHQ4EFgQUGHxYqZYyX7cTxKVODVgZwSTdCnwwHwYDVR0jBBgwFoAUGHxYqZYyX7cTxKVODVgZwSTdCnwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQByFOl+hMFICbd3DJfnp2Rgd/dqttsZG/tyhILWvErbio/DEe98mXpowhTkC04ENprOyXi7ZbUqiicF89uAGyt1oqgTUCD1VsLahqIcmrzgumNyTwLGWo17WDAa1/usDhetWAMhgzF/Cnf5ek0nK00m0YZGyc4LzgD0CROMASTWNg==</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
+ <samlp:Status>
+ <samlp:StatusCode Value='urn:oasis:names:tc:SAML:2.0:status:Success'/>
+ </samlp:Status>
+ <saml:Assertion xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:xs='http://www.w3.org/2001/XMLSchema' ID='_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75' Version='2.0' IssueInstant='2014-07-17T01:01:48Z'>
+ <saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer>
+ <saml:Subject>
+ <saml:NameID SPNameQualifier='http://sp.example.com/demo1/metadata.php' Format='urn:oasis:names:tc:SAML:2.0:nameid-format:transient'>_ce3d2948b4cf20146dee0a0b3dd6f69b6cf86f62d7</saml:NameID>
+ <saml:SubjectConfirmation Method='urn:oasis:names:tc:SAML:2.0:cm:bearer'>
+ <saml:SubjectConfirmationData NotOnOrAfter='2024-01-18T06:21:48Z' Recipient='http://sp.example.com/demo1/index.php?acs' InResponseTo='ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685'/>
+ </saml:SubjectConfirmation>
+ </saml:Subject>
+ <saml:Conditions NotBefore='2014-07-17T01:01:18Z' NotOnOrAfter='2024-01-18T06:21:48Z'>
+ <saml:AudienceRestriction>
+ <saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience>
+ </saml:AudienceRestriction>
+ </saml:Conditions>
+ <saml:AuthnStatement AuthnInstant='2014-07-17T01:01:48Z' SessionNotOnOrAfter='2024-07-17T09:01:48Z' SessionIndex='_be9967abd904ddcae3c0eb4189adbe3f71e327cf93'>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+ </saml:AuthnStatement>
+ <saml:AttributeStatement>
+ <saml:Attribute Name='uid' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='mail' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>test@example.com</saml:AttributeValue>
+ </saml:Attribute>
+ <saml:Attribute Name='eduPersonAffiliation' NameFormat='urn:oasis:names:tc:SAML:2.0:attrname-format:basic'>
+ <saml:AttributeValue xsi:type='xs:string'>users</saml:AttributeValue>
+ <saml:AttributeValue xsi:type='xs:string'>examplerole1</saml:AttributeValue>
+ </saml:Attribute>
+ </saml:AttributeStatement>
+ </saml:Assertion>
+</samlp:Response>
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
deleted file mode 100644
index bef7e2ff8ee..00000000000
--- a/spec/fixtures/exported-project.gz
+++ /dev/null
Binary files differ
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
new file mode 100644
index 00000000000..6e57122867a
--- /dev/null
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Types::MergeRequestType do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+end
diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb
new file mode 100644
index 00000000000..a7e51797047
--- /dev/null
+++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::BasePermissionType do
+ let(:permitable) { double('permittable') }
+ let(:current_user) { build(:user) }
+ let(:context) { { current_user: current_user } }
+ subject(:test_type) do
+ Class.new(described_class) do
+ graphql_name 'TestClass'
+
+ permission_field :do_stuff, resolve: -> (_, _, _) { true }
+ ability_field(:read_issue)
+ abilities :admin_issue
+ end
+ end
+
+ describe '.permission_field' do
+ it 'adds a field for the required permission' do
+ is_expected.to have_graphql_field(:do_stuff)
+ end
+ end
+
+ describe '.ability_field' do
+ it 'adds a field for the required permission' do
+ is_expected.to have_graphql_field(:read_issue)
+ end
+
+ it 'does not add a resolver block if another resolving param is passed' do
+ expected_keywords = {
+ name: :resolve_using_hash,
+ hash_key: :the_key,
+ type: GraphQL::BOOLEAN_TYPE,
+ description: "custom description",
+ null: false
+ }
+ expect(test_type).to receive(:field).with(expected_keywords)
+
+ test_type.ability_field :resolve_using_hash, hash_key: :the_key, description: "custom description"
+ end
+ end
+
+ describe '.abilities' do
+ it 'adds a field for the passed permissions' do
+ is_expected.to have_graphql_field(:admin_issue)
+ end
+ end
+end
diff --git a/spec/graphql/types/permission_types/merge_request_spec.rb b/spec/graphql/types/permission_types/merge_request_spec.rb
new file mode 100644
index 00000000000..e1026b01a74
--- /dev/null
+++ b/spec/graphql/types/permission_types/merge_request_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::MergeRequest do
+ it do
+ expected_permissions = [
+ :read_merge_request, :admin_merge_request, :update_merge_request,
+ :create_note, :push_to_source_branch, :remove_source_branch,
+ :cherry_pick_on_current_merge_request, :revert_on_current_merge_request
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions)
+ end
+end
diff --git a/spec/graphql/types/permission_types/merge_request_type_spec.rb b/spec/graphql/types/permission_types/merge_request_type_spec.rb
new file mode 100644
index 00000000000..6e57122867a
--- /dev/null
+++ b/spec/graphql/types/permission_types/merge_request_type_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe Types::MergeRequestType do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
+end
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
new file mode 100644
index 00000000000..89eecef096e
--- /dev/null
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Types::PermissionTypes::Project do
+ it do
+ expected_permissions = [
+ :change_namespace, :change_visibility_level, :rename_project, :remove_project, :archive_project,
+ :remove_fork_project, :remove_pages, :read_project, :create_merge_request_in,
+ :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics,
+ :download_code, :download_wiki_code, :fork_project, :create_project_snippet,
+ :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule,
+ :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
+ :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
+ :update_wiki, :destroy_wiki, :create_pages, :destroy_pages
+ ]
+
+ expect(described_class).to have_graphql_fields(expected_permissions)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index b4eeca2e3f1..7b5bc335511 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe GitlabSchema.types['Project'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Project) }
+
it { expect(described_class.graphql_name).to eq('Project') }
describe 'nested merge request' do
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 593b2ca1825..14297a1a544 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -157,7 +157,7 @@ describe ApplicationHelper do
let(:noteable_type) { Issue }
it 'returns paths for autocomplete_sources_controller' do
sources = helper.autocomplete_data_sources(project, noteable_type)
- expect(sources.keys).to match_array([:members, :issues, :merge_requests, :labels, :milestones, :commands])
+ expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands])
sources.keys.each do |key|
expect(sources[key]).not_to be_nil
end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 3008528e60c..885204062fe 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -54,7 +54,7 @@ describe MergeRequestsHelper do
let(:options) { { force_link: true } }
it 'removes the data-toggle attributes' do
- is_expected.not_to match(/data-toggle="tab"/)
+ is_expected.not_to match(/data-toggle="tabvue"/)
end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 5cf9e9e8f12..80147b13739 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -248,7 +248,7 @@ describe ProjectsHelper do
describe '#link_to_member' do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
- let(:user) { build_stubbed(:user) }
+ let(:user) { build_stubbed(:user, name: '<h1>Administrator</h1>') }
describe 'using the default options' do
it 'returns an HTML link to the user' do
@@ -256,6 +256,13 @@ describe ProjectsHelper do
expect(link).to match(%r{/#{user.username}})
end
+
+ it 'HTML escapes the name of the user' do
+ link = helper.link_to_member(project, user)
+
+ expect(link).to include(ERB::Util.html_escape(user.name))
+ expect(link).not_to include(user.name)
+ end
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 8d9dc092547..f96e5a2133f 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -44,49 +44,6 @@ describe '6_validations' do
end
end
- describe 'validate_storages_paths' do
- context 'with correct settings' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/d'))
- end
-
- it 'passes through' do
- expect { validate_storages_paths }.not_to raise_error
- end
- end
-
- context 'with nested storage paths' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c/d'))
- end
-
- it 'throws an error' do
- expect { validate_storages_paths }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
- end
- end
-
- context 'with similar but un-nested storage paths' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c'), 'bar' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/paths/a/b/c2'))
- end
-
- it 'passes through' do
- expect { validate_storages_paths }.not_to raise_error
- end
- end
-
- describe 'inaccessible storage' do
- before do
- mock_storages('foo' => Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/tests/a/path/that/does/not/exist'))
- end
-
- it 'passes through with a warning' do
- expect(Rails.logger).to receive(:error)
- expect { validate_storages_paths }.not_to raise_error
- end
- end
- end
-
def mock_storages(storages)
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index e8435116221..54cb6d84109 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -242,7 +242,7 @@ describe('Api', () => {
},
]);
- Api.groupProjects(groupId, query, response => {
+ Api.groupProjects(groupId, query, {}, response => {
expect(response.length).toBe(1);
expect(response[0].name).toBe('test');
done();
@@ -362,4 +362,29 @@ describe('Api', () => {
.catch(done.fail);
});
});
+
+ describe('createBranch', () => {
+ it('creates new branch', done => {
+ const ref = 'master';
+ const branch = 'new-branch-name';
+ const dummyProjectPath = 'gitlab-org/gitlab-ce';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
+ dummyProjectPath,
+ )}/repository/branches`;
+
+ spyOn(axios, 'post').and.callThrough();
+
+ mock.onPost(expectedUrl).replyOnce(200, {
+ name: branch,
+ });
+
+ Api.createBranch(dummyProjectPath, { ref, branch })
+ .then(({ data }) => {
+ expect(data.name).toBe(branch);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, { ref, branch });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index efbe09a10a2..c2db81c6ce4 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -44,4 +44,59 @@ describe('CopyAsGFM', () => {
callPasteGFM();
});
});
+
+ describe('CopyAsGFM.copyGFM', () => {
+ // Stub getSelection to return a purpose-built object.
+ const stubSelection = (html, parentNode) => ({
+ getRangeAt: () => ({
+ commonAncestorContainer: { tagName: parentNode },
+ cloneContents: () => {
+ const fragment = document.createDocumentFragment();
+ const node = document.createElement('div');
+ node.innerHTML = html;
+ Array.from(node.childNodes).forEach((item) => fragment.appendChild(item));
+ return fragment;
+ },
+ }),
+ rangeCount: 1,
+ });
+
+ const clipboardData = {
+ setData() {},
+ };
+
+ const simulateCopy = () => {
+ const e = {
+ originalEvent: {
+ clipboardData,
+ },
+ preventDefault() {},
+ stopPropagation() {},
+ };
+ CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
+ return clipboardData;
+ };
+
+ beforeEach(() => spyOn(clipboardData, 'setData'));
+
+ describe('list handling', () => {
+ it('uses correct gfm for unordered lists', () => {
+ const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'UL');
+ spyOn(window, 'getSelection').and.returnValue(selection);
+ simulateCopy();
+
+ const expectedGFM = '- List Item1\n- List Item2';
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ });
+
+ it('uses correct gfm for ordered lists', () => {
+ const selection = stubSelection('<li>List Item1</li><li>List Item2</li>\n', 'OL');
+ spyOn(window, 'getSelection').and.returnValue(selection);
+ simulateCopy();
+
+ const expectedGFM = '1. List Item1\n1. List Item2';
+ expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
index d1ebae33dab..7651792be2e 100644
--- a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
+++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
@@ -26,7 +26,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(10, 10, 10),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).not.toBeGreaterThan(4);
});
@@ -35,7 +35,7 @@ describe('Mesh object', () => {
const object = new MeshObject(
new BoxGeometry(1, 1, 1),
);
- const radius = object.geometry.boundingSphere.radius;
+ const { radius } = object.geometry.boundingSphere;
expect(radius).toBeLessThan(1);
});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 05acf903933..7a32e84bced 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -9,7 +9,7 @@ import '~/vue_shared/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/stores/boards_store';
-import '~/boards/components/issue_card_inner';
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj } from './mock_data';
describe('Issue card component', () => {
@@ -48,7 +48,7 @@ describe('Issue card component', () => {
component = new Vue({
el: document.querySelector('.test-container'),
components: {
- 'issue-card': gl.issueBoards.IssueCardInner,
+ 'issue-card': IssueCardInner,
},
data() {
return {
@@ -255,7 +255,7 @@ describe('Issue card component', () => {
it('renders label', () => {
const nodes = [];
component.$el.querySelectorAll('.badge').forEach((label) => {
- nodes.push(label.title);
+ nodes.push(label.getAttribute('data-original-title'));
});
expect(
@@ -265,7 +265,7 @@ describe('Issue card component', () => {
it('sets label description as title', () => {
expect(
- component.$el.querySelector('.badge').getAttribute('title'),
+ component.$el.querySelector('.badge').getAttribute('data-original-title'),
).toContain(label1.description);
});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 819ed7896ca..a18e09da50a 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -16,7 +16,7 @@ describe('Pipelines table in Commits and Merge requests', function () {
beforeEach(() => {
mock = new MockAdapter(axios);
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTable = Vue.extend(pipelinesTable);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js
index 4279add21d1..d1de9d132b8 100644
--- a/spec/javascripts/deploy_keys/components/key_spec.js
+++ b/spec/javascripts/deploy_keys/components/key_spec.js
@@ -88,7 +88,7 @@ describe('Deploy keys key', () => {
});
it('expands all project labels after click', done => {
- const length = vm.deployKey.deploy_keys_projects.length;
+ const { length } = vm.deployKey.deploy_keys_projects;
vm.$el.querySelectorAll('.deploy-project-label')[1].click();
Vue.nextTick(() => {
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index 7237274eb43..dea600a783a 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -1 +1,95 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
+import Vue from 'vue';
+import DiffContentComponent from '~/diffs/components/diff_content.vue';
+import store from '~/mr_notes/stores';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
+import diffFileMockData from '../mock_data/diff_file';
+
+describe('DiffContent', () => {
+ const Component = Vue.extend(DiffContentComponent);
+ let vm;
+ const getDiffFileMock = () => Object.assign({}, diffFileMockData);
+
+ beforeEach(() => {
+ vm = mountComponentWithStore(Component, {
+ store,
+ props: {
+ diffFile: getDiffFileMock(),
+ },
+ });
+ });
+
+ describe('text based files', () => {
+ it('should render diff inline view', done => {
+ vm.$store.state.diffs.diffViewType = 'inline';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(1);
+
+ done();
+ });
+ });
+
+ it('should render diff parallel view', done => {
+ vm.$store.state.diffs.diffViewType = 'parallel';
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.parallel').length).toEqual(18);
+
+ done();
+ });
+ });
+ });
+
+ describe('Non-Text diffs', () => {
+ beforeEach(() => {
+ vm.diffFile.text = false;
+ });
+
+ describe('image diff', () => {
+ beforeEach(() => {
+ vm.diffFile.newPath = GREEN_BOX_IMAGE_URL;
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = RED_BOX_IMAGE_URL;
+ vm.diffFile.oldSha = 'ABC';
+ vm.diffFile.viewPath = '';
+ });
+
+ it('should have image diff view in place', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(vm.$el.querySelectorAll('.diff-viewer .image').length).toEqual(1);
+
+ done();
+ });
+ });
+ });
+
+ describe('file diff', () => {
+ it('should have download buttons in place', done => {
+ const el = vm.$el;
+ vm.diffFile.newPath = 'test.abc';
+ vm.diffFile.newSha = 'DEF';
+ vm.diffFile.oldPath = 'test.abc';
+ vm.diffFile.oldSha = 'ABC';
+
+ vm.$nextTick(() => {
+ expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0);
+
+ expect(el.querySelector('.deleted .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ expect(el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc');
+ expect(el.querySelector('.added .btn.btn-default').textContent.trim()).toContain(
+ 'Download',
+ );
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
index 312a684f4d2..2d136a63c52 100644
--- a/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_gutter_content_spec.js
@@ -2,12 +2,6 @@ import Vue from 'vue';
import DiffLineGutterContent from '~/diffs/components/diff_line_gutter_content.vue';
import store from '~/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
-import {
- MATCH_LINE_TYPE,
- CONTEXT_LINE_TYPE,
- OLD_NO_NEW_LINE_TYPE,
- NEW_NO_NEW_LINE_TYPE,
-} from '~/diffs/constants';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@@ -31,45 +25,6 @@ describe('DiffLineGutterContent', () => {
};
describe('computed', () => {
- describe('isMatchLine', () => {
- it('should return true for match line type', () => {
- const component = createComponent({ lineType: MATCH_LINE_TYPE });
- expect(component.isMatchLine).toEqual(true);
- });
-
- it('should return false for non-match line type', () => {
- const component = createComponent({ lineType: CONTEXT_LINE_TYPE });
- expect(component.isMatchLine).toEqual(false);
- });
- });
-
- describe('isContextLine', () => {
- it('should return true for context line type', () => {
- const component = createComponent({ lineType: CONTEXT_LINE_TYPE });
- expect(component.isContextLine).toEqual(true);
- });
-
- it('should return false for non-context line type', () => {
- const component = createComponent({ lineType: MATCH_LINE_TYPE });
- expect(component.isContextLine).toEqual(false);
- });
- });
-
- describe('isMetaLine', () => {
- it('should return true for meta line type', () => {
- const component = createComponent({ lineType: NEW_NO_NEW_LINE_TYPE });
- expect(component.isMetaLine).toEqual(true);
-
- const component2 = createComponent({ lineType: OLD_NO_NEW_LINE_TYPE });
- expect(component2.isMetaLine).toEqual(true);
- });
-
- it('should return false for non-meta line type', () => {
- const component = createComponent({ lineType: MATCH_LINE_TYPE });
- expect(component.isMetaLine).toEqual(false);
- });
- });
-
describe('lineHref', () => {
it('should prepend # to lineCode', () => {
const lineCode = 'LC_42';
@@ -92,7 +47,7 @@ describe('DiffLineGutterContent', () => {
});
it('should return discussions for the given lineCode', () => {
- const lineCode = getDiffFileMock().highlightedDiffLines[1].lineCode;
+ const { lineCode } = getDiffFileMock().highlightedDiffLines[1];
const component = createComponent({ lineCode, showCommentButton: true });
setDiscussions(component);
@@ -109,7 +64,7 @@ describe('DiffLineGutterContent', () => {
describe('template', () => {
it('should render three dots for context lines', () => {
const component = createComponent({
- lineType: MATCH_LINE_TYPE,
+ isMatchLine: true,
});
expect(component.$el.querySelector('span').classList.contains('context-cell')).toEqual(true);
diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
index 724d1948214..81cd4f9769a 100644
--- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js
+++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js
@@ -19,7 +19,15 @@ describe('DiffLineNoteForm', () => {
diffLines,
line: diffLines[0],
noteTargetLine: diffLines[0],
- }).$mount();
+ });
+
+ Object.defineProperty(component, 'isLoggedIn', {
+ get() {
+ return true;
+ },
+ });
+
+ component.$mount();
});
describe('methods', () => {
@@ -56,6 +64,15 @@ describe('DiffLineNoteForm', () => {
});
});
+ describe('mounted', () => {
+ it('should init autosave', () => {
+ const key = 'autosave/Note/issue///DiffNote//1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_1';
+
+ expect(component.autosave).toBeDefined();
+ expect(component.autosave.key).toEqual(key);
+ });
+ });
+
describe('template', () => {
it('should have note form', () => {
const { $el } = component;
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index 0d5a3576204..e1adf60962e 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import store from '~/mr_notes/stores';
-import * as constants from '~/diffs/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
@@ -14,58 +13,13 @@ describe('InlineDiffView', () => {
beforeEach(() => {
const diffFile = getDiffFileMock();
+ store.dispatch('setInlineDiffViewType');
component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
diffFile,
diffLines: diffFile.highlightedDiffLines,
}).$mount();
});
- describe('methods', () => {
- describe('handleMouse', () => {
- it('should set hoveredLineCode', () => {
- expect(component.hoveredLineCode).toEqual(null);
-
- component.handleMouse('lineCode1', true);
- expect(component.hoveredLineCode).toEqual('lineCode1');
-
- component.handleMouse('lineCode1', false);
- expect(component.hoveredLineCode).toEqual(null);
- });
- });
-
- describe('getLineClass', () => {
- it('should return line class object', () => {
- const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants;
- const { MATCH_LINE_TYPE, NEW_LINE_TYPE } = constants;
-
- expect(component.getLineClass(component.diffLines[0])).toEqual({
- [NEW_LINE_TYPE]: NEW_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: false,
- [LINE_HOVER_CLASS_NAME]: false,
- });
-
- component.handleMouse(component.diffLines[0].lineCode, true);
- Object.defineProperty(component, 'isLoggedIn', {
- get() {
- return true;
- },
- });
-
- expect(component.getLineClass(component.diffLines[0])).toEqual({
- [NEW_LINE_TYPE]: NEW_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: false,
- [LINE_HOVER_CLASS_NAME]: true,
- });
-
- expect(component.getLineClass(component.diffLines[5])).toEqual({
- [MATCH_LINE_TYPE]: MATCH_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: true,
- [LINE_HOVER_CLASS_NAME]: false,
- });
- });
- });
- });
-
describe('template', () => {
it('should have rendered diff lines', () => {
const el = component.$el;
@@ -89,23 +43,5 @@ describe('InlineDiffView', () => {
done();
});
});
-
- it('should render new discussion forms', done => {
- const el = component.$el;
- const lines = getDiffFileMock().highlightedDiffLines;
-
- component.handleShowCommentForm({ lineCode: lines[0].lineCode });
- component.handleShowCommentForm({ lineCode: lines[1].lineCode });
-
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2);
- expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true);
- expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true);
-
- store.state.diffs.diffLineCommentForms = {};
-
- done();
- });
- });
});
});
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
index cab533217c0..165e4b69b6c 100644
--- a/spec/javascripts/diffs/components/parallel_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -4,12 +4,10 @@ import store from '~/mr_notes/stores';
import * as constants from '~/diffs/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
-import discussionsMockData from '../mock_data/diff_discussions';
describe('ParallelDiffView', () => {
let component;
const getDiffFileMock = () => Object.assign({}, diffFileMockData);
- const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
beforeEach(() => {
const diffFile = getDiffFileMock();
@@ -28,197 +26,4 @@ describe('ParallelDiffView', () => {
});
});
});
-
- describe('methods', () => {
- describe('hasDiscussion', () => {
- it('it should return true if there is a discussion either for left or right section', () => {
- Object.defineProperty(component, 'discussionsByLineCode', {
- get() {
- return { line_42: true };
- },
- });
-
- expect(component.hasDiscussion({ left: {}, right: {} })).toEqual(undefined);
- expect(component.hasDiscussion({ left: { lineCode: 'line_42' }, right: {} })).toEqual(true);
- expect(component.hasDiscussion({ left: {}, right: { lineCode: 'line_42' } })).toEqual(true);
- });
- });
-
- describe('getClassName', () => {
- it('should return line class object', () => {
- const { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME } = constants;
- const { MATCH_LINE_TYPE, NEW_LINE_TYPE, LINE_POSITION_RIGHT } = constants;
-
- expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({
- [NEW_LINE_TYPE]: NEW_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: false,
- [LINE_HOVER_CLASS_NAME]: false,
- });
-
- const eventMock = {
- target: component.$refs.rightLines[1],
- };
-
- component.handleMouse(eventMock, component.diffLines[1], true);
- Object.defineProperty(component, 'isLoggedIn', {
- get() {
- return true;
- },
- });
-
- expect(component.getClassName(component.diffLines[1], LINE_POSITION_RIGHT)).toEqual({
- [NEW_LINE_TYPE]: NEW_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: false,
- [LINE_HOVER_CLASS_NAME]: true,
- });
-
- expect(component.getClassName(component.diffLines[5], LINE_POSITION_RIGHT)).toEqual({
- [MATCH_LINE_TYPE]: MATCH_LINE_TYPE,
- [LINE_UNFOLD_CLASS_NAME]: true,
- [LINE_HOVER_CLASS_NAME]: false,
- });
- });
- });
-
- describe('handleMouse', () => {
- it('should set hovered line code and line section to null when isHover is false', () => {
- const rightLineEventMock = { target: component.$refs.rightLines[1] };
- expect(component.hoveredLineCode).toEqual(null);
- expect(component.hoveredSection).toEqual(null);
-
- component.handleMouse(rightLineEventMock, null, false);
- expect(component.hoveredLineCode).toEqual(null);
- expect(component.hoveredSection).toEqual(null);
- });
-
- it('should set hovered line code and line section for right section', () => {
- const rightLineEventMock = { target: component.$refs.rightLines[1] };
- component.handleMouse(rightLineEventMock, component.diffLines[1], true);
- expect(component.hoveredLineCode).toEqual(component.diffLines[1].right.lineCode);
- expect(component.hoveredSection).toEqual(constants.LINE_POSITION_RIGHT);
- });
-
- it('should set hovered line code and line section for left section', () => {
- const leftLineEventMock = { target: component.$refs.leftLines[2] };
- component.handleMouse(leftLineEventMock, component.diffLines[2], true);
- expect(component.hoveredLineCode).toEqual(component.diffLines[2].left.lineCode);
- expect(component.hoveredSection).toEqual(constants.LINE_POSITION_LEFT);
- });
- });
-
- describe('shouldRenderDiscussions', () => {
- it('should return true if there is a discussion on left side and it is expanded', () => {
- const line = { left: { lineCode: 'lineCode1' } };
- spyOn(component, 'isDiscussionExpanded').and.returnValue(true);
- Object.defineProperty(component, 'discussionsByLineCode', {
- get() {
- return {
- [line.left.lineCode]: true,
- };
- },
- });
-
- expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(true);
- expect(component.isDiscussionExpanded).toHaveBeenCalledWith(line.left.lineCode);
- });
-
- it('should return false if there is a discussion on left side but it is collapsed', () => {
- const line = { left: { lineCode: 'lineCode1' } };
- spyOn(component, 'isDiscussionExpanded').and.returnValue(false);
- Object.defineProperty(component, 'discussionsByLineCode', {
- get() {
- return {
- [line.left.lineCode]: true,
- };
- },
- });
-
- expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_LEFT)).toEqual(
- false,
- );
- });
-
- it('should return false for discussions on the right side if there is no line type', () => {
- const CUSTOM_RIGHT_LINE_TYPE = 'CUSTOM_RIGHT_LINE_TYPE';
- const line = { right: { lineCode: 'lineCode1', type: CUSTOM_RIGHT_LINE_TYPE } };
- spyOn(component, 'isDiscussionExpanded').and.returnValue(true);
- Object.defineProperty(component, 'discussionsByLineCode', {
- get() {
- return {
- [line.right.lineCode]: true,
- };
- },
- });
-
- expect(component.shouldRenderDiscussions(line, constants.LINE_POSITION_RIGHT)).toEqual(
- CUSTOM_RIGHT_LINE_TYPE,
- );
- });
- });
-
- describe('hasAnyExpandedDiscussion', () => {
- const LINE_CODE_LEFT = 'LINE_CODE_LEFT';
- const LINE_CODE_RIGHT = 'LINE_CODE_RIGHT';
-
- it('should return true if there is a discussion either on the left or the right side', () => {
- const mockLineOne = {
- right: { lineCode: LINE_CODE_RIGHT },
- left: {},
- };
- const mockLineTwo = {
- left: { lineCode: LINE_CODE_LEFT },
- right: {},
- };
-
- spyOn(component, 'isDiscussionExpanded').and.callFake(lc => lc === LINE_CODE_RIGHT);
- expect(component.hasAnyExpandedDiscussion(mockLineOne)).toEqual(true);
- expect(component.hasAnyExpandedDiscussion(mockLineTwo)).toEqual(false);
- });
- });
- });
-
- describe('template', () => {
- it('should have rendered diff lines', () => {
- const el = component.$el;
-
- expect(el.querySelectorAll('tr.line_holder.parallel').length).toEqual(6);
- expect(el.querySelectorAll('td.empty-cell').length).toEqual(4);
- expect(el.querySelectorAll('td.line_content.parallel.right-side').length).toEqual(6);
- expect(el.querySelectorAll('td.line_content.parallel.left-side').length).toEqual(6);
- expect(el.querySelectorAll('td.match').length).toEqual(4);
- expect(el.textContent.indexOf('Bad dates') > -1).toEqual(true);
- });
-
- it('should render discussions', done => {
- const el = component.$el;
- component.$store.dispatch('setInitialNotes', getDiscussionsMockData());
-
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.notes_holder').length).toEqual(1);
- expect(el.querySelectorAll('.notes_holder .note-discussion li').length).toEqual(5);
- expect(el.innerText.indexOf('comment 5') > -1).toEqual(true);
- component.$store.dispatch('setInitialNotes', []);
-
- done();
- });
- });
-
- it('should render new discussion forms', done => {
- const el = component.$el;
- const lines = getDiffFileMock().parallelDiffLines;
-
- component.handleShowCommentForm({ lineCode: lines[0].lineCode });
- component.handleShowCommentForm({ lineCode: lines[1].lineCode });
-
- Vue.nextTick(() => {
- expect(el.querySelectorAll('.js-vue-markdown-field').length).toEqual(2);
- expect(el.querySelectorAll('tr')[1].classList.contains('notes_holder')).toEqual(true);
- expect(el.querySelectorAll('tr')[3].classList.contains('notes_holder')).toEqual(true);
-
- store.state.diffs.diffLineCommentForms = {};
-
- done();
- });
- });
- });
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index e61780c9928..6829c1e956a 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -5,38 +5,22 @@ import {
INLINE_DIFF_VIEW_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
} from '~/diffs/constants';
-import store from '~/diffs/store';
import * as actions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
import testAction from '../../helpers/vuex_action_helper';
describe('DiffsStoreActions', () => {
- describe('setEndpoint', () => {
- it('should set given endpoint', done => {
+ describe('setBaseConfig', () => {
+ it('should set given endpoint and project path', done => {
const endpoint = '/diffs/set/endpoint';
+ const projectPath = '/root/project';
testAction(
- actions.setEndpoint,
- endpoint,
- { endpoint: '' },
- [{ type: types.SET_ENDPOINT, payload: endpoint }],
- [],
- done,
- );
- });
- });
-
- describe('setLoadingState', () => {
- it('should set loading state', done => {
- expect(store.state.diffs.isLoading).toEqual(true);
- const loadingState = false;
-
- testAction(
- actions.setLoadingState,
- loadingState,
- {},
- [{ type: types.SET_LOADING, payload: loadingState }],
+ actions.setBaseConfig,
+ { endpoint, projectPath },
+ { endpoint: '', projectPath: '' },
+ [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }],
[],
done,
);
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 5f1a6e9def7..02836fcaeea 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -3,13 +3,15 @@ import * as types from '~/diffs/store/mutation_types';
import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
describe('DiffsStoreMutations', () => {
- describe('SET_ENDPOINT', () => {
- it('should set endpoint', () => {
+ describe('SET_BASE_CONFIG', () => {
+ it('should set endpoint and project path', () => {
const state = {};
const endpoint = '/diffs/endpoint';
+ const projectPath = '/root/project';
- mutations[types.SET_ENDPOINT](state, endpoint);
+ mutations[types.SET_BASE_CONFIG](state, { endpoint, projectPath });
expect(state.endpoint).toEqual(endpoint);
+ expect(state.projectPath).toEqual(projectPath);
});
});
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 59bd2650081..d926663fac0 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -103,7 +103,7 @@ describe('RecentSearchesDropdownContent', () => {
describe('processedItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(2);
@@ -122,7 +122,7 @@ describe('RecentSearchesDropdownContent', () => {
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const processedItems = vm.processedItems;
+ const { processedItems } = vm;
expect(processedItems.length).toEqual(0);
});
@@ -131,13 +131,13 @@ describe('RecentSearchesDropdownContent', () => {
describe('hasItems', () => {
it('with items', () => {
vm = createComponent(propsDataWithItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(true);
});
it('with no items', () => {
vm = createComponent(propsDataWithoutItems);
- const hasItems = vm.hasItems;
+ const { hasItems } = vm;
expect(hasItems).toEqual(false);
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
index fbc3926d332..68158cf52e4 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
@@ -17,6 +17,17 @@ describe('Filtered Search Token Keys', () => {
});
});
+ describe('getKeys', () => {
+ it('should return keys', () => {
+ const getKeys = FilteredSearchTokenKeys.getKeys();
+ const keys = FilteredSearchTokenKeys.get().map(i => i.key);
+
+ keys.forEach((key, i) => {
+ expect(key).toEqual(getKeys[i]);
+ });
+ });
+ });
+
describe('getConditions', () => {
let conditions;
diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/javascripts/filtered_search/recent_searches_root_spec.js
index 1e6272bad0b..d063fcf4f2d 100644
--- a/spec/javascripts/filtered_search/recent_searches_root_spec.js
+++ b/spec/javascripts/filtered_search/recent_searches_root_spec.js
@@ -15,8 +15,7 @@ describe('RecentSearchesRoot', () => {
};
VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake((options) => {
- data = options.data;
- template = options.template;
+ ({ data, template } = options);
});
RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index 2839020b2ca..21c462cd040 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -18,7 +18,7 @@ describe('GL Style Field Errors', function() {
expect(this.$form).toBeDefined();
expect(this.$form.length).toBe(1);
expect(this.fieldErrors).toBeDefined();
- const inputs = this.fieldErrors.state.inputs;
+ const { inputs } = this.fieldErrors.state;
expect(inputs.length).toBe(4);
});
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 2b92c485f41..03d4b472b87 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -67,7 +67,7 @@ describe('AppComponent', () => {
it('should return list of groups from store', () => {
spyOn(vm.store, 'getGroups');
- const groups = vm.groups;
+ const { groups } = vm;
expect(vm.store.getGroups).toHaveBeenCalled();
expect(groups).not.toBeDefined();
});
@@ -77,7 +77,7 @@ describe('AppComponent', () => {
it('should return pagination info from store', () => {
spyOn(vm.store, 'getPaginationInfo');
- const pageInfo = vm.pageInfo;
+ const { pageInfo } = vm;
expect(vm.store.getPaginationInfo).toHaveBeenCalled();
expect(pageInfo).not.toBeDefined();
});
@@ -293,7 +293,7 @@ describe('AppComponent', () => {
beforeEach(() => {
groupItem = Object.assign({}, mockParentGroupItem);
groupItem.children = mockChildren;
- childGroupItem = groupItem.children[0];
+ [childGroupItem] = groupItem.children;
groupItem.isChildrenLoading = false;
vm.targetGroup = childGroupItem;
vm.targetParentGroup = groupItem;
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 49a139855c8..d0cac5efc40 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -41,7 +41,7 @@ describe('GroupItemComponent', () => {
describe('rowClass', () => {
it('should return map of classes based on group details', () => {
const classes = ['is-open', 'has-children', 'has-description', 'being-removed'];
- const rowClass = vm.rowClass;
+ const { rowClass } = vm;
expect(Object.keys(rowClass).length).toBe(classes.length);
Object.keys(rowClass).forEach((className) => {
diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js
index 921d42a0871..fc4288eb15b 100644
--- a/spec/javascripts/helpers/init_vue_mr_page_helper.js
+++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js
@@ -5,11 +5,16 @@ import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_dat
import diffFileMockData from '../diffs/mock_data/diff_file';
export default function initVueMRPage() {
+ const mrTestEl = document.createElement('div');
+ mrTestEl.className = 'js-merge-request-test';
+ document.body.appendChild(mrTestEl);
+
const diffsAppEndpoint = '/diffs/app/endpoint';
+ const diffsAppProjectPath = 'testproject';
const mrEl = document.createElement('div');
mrEl.className = 'merge-request fixture-mr';
mrEl.setAttribute('data-mr-action', 'diffs');
- document.body.appendChild(mrEl);
+ mrTestEl.appendChild(mrEl);
const mrDiscussionsEl = document.createElement('div');
mrDiscussionsEl.id = 'js-vue-mr-discussions';
@@ -17,17 +22,18 @@ export default function initVueMRPage() {
mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
- document.body.appendChild(mrDiscussionsEl);
+ mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
discussionCounterEl.id = 'js-vue-discussion-counter';
- document.body.appendChild(discussionCounterEl);
+ mrTestEl.appendChild(discussionCounterEl);
const diffsAppEl = document.createElement('div');
diffsAppEl.id = 'js-diffs-app';
diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
+ diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
- document.body.appendChild(diffsAppEl);
+ mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
mock.onGet(diffsAppEndpoint).reply(200, {
diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
index 8b47a365582..b7a7afe4db4 100644
--- a/spec/javascripts/ide/components/commit_sidebar/form_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
@@ -16,6 +16,7 @@ describe('IDE commit form', () => {
store.state.changedFiles.push('test');
store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
Vue.set(store.state.projects, 'abcproject', { ...projectData });
vm = createComponentWithStore(Component, store).$mount();
@@ -146,4 +147,16 @@ describe('IDE commit form', () => {
});
});
});
+
+ describe('commitButtonText', () => {
+ it('returns commit text when staged files exist', () => {
+ vm.$store.state.stagedFiles.push('testing');
+
+ expect(vm.commitButtonText).toBe('Commit');
+ });
+
+ it('returns stage & commit text when staged files do not exist', () => {
+ expect(vm.commitButtonText).toBe('Stage & Commit');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
index d62d58101d6..942cc19f46d 100644
--- a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js
@@ -13,6 +13,7 @@ describe('IDE commit message field', () => {
Component,
{
text: '',
+ placeholder: 'testing',
},
'#app',
);
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
index 21bfe4be52f..ffc2a4c9ddb 100644
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -114,4 +114,19 @@ describe('IDE commit sidebar radio group', () => {
});
});
});
+
+ describe('tooltipTitle', () => {
+ it('returns title when disabled', () => {
+ vm.title = 'test title';
+ vm.disabled = true;
+
+ expect(vm.tooltipTitle).toBe('test title');
+ });
+
+ it('returns blank when not disabled', () => {
+ vm.title = 'test title';
+
+ expect(vm.tooltipTitle).not.toBe('test title');
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
new file mode 100644
index 00000000000..430e8e2baa3
--- /dev/null
+++ b/spec/javascripts/ide/components/error_message_spec.js
@@ -0,0 +1,106 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ErrorMessage from '~/ide/components/error_message.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { resetStore } from '../helpers';
+
+describe('IDE error message component', () => {
+ const Component = Vue.extend(ErrorMessage);
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponentWithStore(Component, store, {
+ message: {
+ text: 'error message',
+ action: null,
+ actionText: null,
+ },
+ }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ resetStore(vm.$store);
+ });
+
+ it('renders error message', () => {
+ expect(vm.$el.textContent).toContain('error message');
+ });
+
+ it('clears error message on click', () => {
+ spyOn(vm, 'setErrorMessage');
+
+ vm.$el.click();
+
+ expect(vm.setErrorMessage).toHaveBeenCalledWith(null);
+ });
+
+ describe('with action', () => {
+ let actionSpy;
+
+ beforeEach(done => {
+ actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve());
+
+ vm.message.action = actionSpy;
+ vm.message.actionText = 'test action';
+ vm.message.actionPayload = 'testActionPayload';
+
+ vm.$nextTick(done);
+ });
+
+ it('renders action button', () => {
+ expect(vm.$el.querySelector('.flash-action')).not.toBe(null);
+ expect(vm.$el.textContent).toContain('test action');
+ });
+
+ it('does not clear error message on click', () => {
+ spyOn(vm, 'setErrorMessage');
+
+ vm.$el.click();
+
+ expect(vm.setErrorMessage).not.toHaveBeenCalled();
+ });
+
+ it('dispatches action', done => {
+ vm.$el.querySelector('.flash-action').click();
+
+ vm.$nextTick(() => {
+ expect(actionSpy).toHaveBeenCalledWith('testActionPayload');
+
+ done();
+ });
+ });
+
+ it('does not dispatch action when already loading', () => {
+ vm.isLoading = true;
+
+ vm.$el.querySelector('.flash-action').click();
+
+ expect(actionSpy).not.toHaveBeenCalledWith();
+ });
+
+ it('resets isLoading after click', done => {
+ vm.$el.querySelector('.flash-action').click();
+
+ expect(vm.isLoading).toBe(true);
+
+ vm.$nextTick(() => {
+ expect(vm.isLoading).toBe(false);
+
+ done();
+ });
+ });
+
+ it('shows loading icon when isLoading is true', done => {
+ expect(vm.$el.querySelector('.loading-container').style.display).not.toBe('');
+
+ vm.isLoading = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container').style.display).toBe('');
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index 045a60e56a0..708c9fe69af 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -114,4 +114,18 @@ describe('ide component', () => {
expect(vm.mousetrapStopCallback(null, document.querySelector('.inputarea'), 't')).toBe(true);
});
});
+
+ it('shows error message when set', done => {
+ expect(vm.$el.querySelector('.flash-container')).toBe(null);
+
+ vm.$store.state.errorMessage = {
+ text: 'error',
+ };
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 6bf309fb4bf..30cd92b2ca4 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import store from '~/ide/stores';
-import service from '~/ide/services';
import router from '~/ide/ide_router';
import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
@@ -68,23 +67,6 @@ describe('RepoCommitSection', () => {
vm.$mount();
- spyOn(service, 'getTreeData').and.returnValue(
- Promise.resolve({
- headers: {
- 'page-title': 'test',
- },
- json: () =>
- Promise.resolve({
- last_commit_path: 'last_commit_path',
- parent_tree_url: 'parent_tree_url',
- path: '/',
- trees: [{ name: 'tree' }],
- blobs: [{ name: 'blob' }],
- submodules: [{ name: 'submodule' }],
- }),
- }),
- );
-
Vue.nextTick(done);
});
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index 8cabc6e8935..fc0695a4263 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -38,6 +38,26 @@ describe('RepoTab', () => {
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
+ it('does not call openPendingTab when tab is active', done => {
+ vm = createComponent({
+ tab: {
+ ...file(),
+ pending: true,
+ active: true,
+ },
+ });
+
+ spyOn(vm, 'openPendingTab');
+
+ vm.$el.click();
+
+ vm.$nextTick(() => {
+ expect(vm.openPendingTab).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+
it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
@@ -112,9 +132,9 @@ describe('RepoTab', () => {
});
it('renders a tooltip', () => {
- expect(
- vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle,
- ).toContain('Locked by testuser');
+ expect(vm.$el.querySelector('span:nth-child(2)').dataset.originalTitle).toContain(
+ 'Locked by testuser',
+ );
});
});
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
index 9312e17704e..569fa5c7aae 100644
--- a/spec/javascripts/ide/helpers.js
+++ b/spec/javascripts/ide/helpers.js
@@ -1,3 +1,4 @@
+import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils';
import state from '~/ide/stores/state';
import commitState from '~/ide/stores/modules/commit/state';
@@ -14,13 +15,34 @@ export const resetStore = store => {
store.replaceState(newState);
};
-export const file = (name = 'name', id = name, type = '') =>
+export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
id,
type,
icon: 'icon',
url: 'url',
name,
- path: name,
+ path: parent ? `${parent.path}/${name}` : name,
+ parentPath: parent ? parent.path : '',
lastCommit: {},
});
+
+export const createEntriesFromPaths = paths =>
+ paths
+ .map(path => ({
+ name: pathUtils.basename(path),
+ dir: pathUtils.dirname(path),
+ ext: pathUtils.extname(path),
+ }))
+ .reduce((entries, path, idx) => {
+ const { name } = path;
+ const parent = path.dir ? entries[path.dir] : null;
+ const type = path.ext ? 'blob' : 'tree';
+
+ const entry = file(name, (idx + 1).toString(), type, parent);
+
+ return {
+ [entry.path]: entry,
+ ...entries,
+ };
+ }, {});
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index 96abd1dcd9e..90ebb95b687 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -63,7 +63,7 @@ describe('Multi-file editor library dirty diff controller', () => {
[type]: true,
};
- const range = getDecorator(change).range;
+ const { range } = getDecorator(change);
expect(range.startLineNumber).toBe(1);
expect(range.endLineNumber).toBe(2);
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index dd87a43f370..80bf664d491 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -8,6 +8,7 @@ export const projectData = {
branches: {
master: {
treeId: 'abcproject/master',
+ can_push: true,
},
},
mergeRequests: {},
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 5746683917e..58d3ffc6d94 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
@@ -9,11 +11,16 @@ import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store file actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
spyOn(router, 'push');
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
@@ -183,94 +190,125 @@ describe('IDE store file actions', () => {
let localFile;
beforeEach(() => {
- spyOn(service, 'getFileData').and.returnValue(
- Promise.resolve({
- headers: {
- 'page-title': 'testing getFileData',
- },
- json: () =>
- Promise.resolve({
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- raw_path: 'raw_path',
- binary: false,
- html: '123',
- render_error: '',
- }),
- }),
- );
+ spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = 'getFileDataURL';
+ localFile.url = `${gl.TEST_HOST}/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
});
- it('calls the service', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce(
+ 200,
+ {
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ raw_path: 'raw_path',
+ binary: false,
+ html: '123',
+ render_error: '',
+ },
+ {
+ 'page-title': 'testing getFileData',
+ },
+ );
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls the service', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`);
- it('sets the file data', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.blamePath).toBe('blame_path');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file data', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.blamePath).toBe('blame_path');
- it('sets document title', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(document.title).toBe('testing getFileData');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets document title', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(document.title).toBe('testing getFileData');
- it('sets the file as active', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(localFile.active).toBeTruthy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('sets the file as active', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(localFile.active).toBeTruthy();
- it('sets the file not as active if we pass makeFileActive false', done => {
- store
- .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
- .then(() => {
- expect(localFile.active).toBeFalsy();
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
+ it('sets the file not as active if we pass makeFileActive false', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+ .then(() => {
+ expect(localFile.active).toBeFalsy();
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds the file to open files', done => {
+ store
+ .dispatch('getFileData', { path: localFile.path })
+ .then(() => {
+ expect(store.state.openFiles.length).toBe(1);
+ expect(store.state.openFiles[0].name).toBe(localFile.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('adds the file to open files', done => {
- store
- .dispatch('getFileData', { path: localFile.path })
- .then(() => {
- expect(store.state.openFiles.length).toBe(1);
- expect(store.state.openFiles[0].name).toBe(localFile.name);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError();
+ });
- done();
- })
- .catch(done.fail);
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: localFile.path,
+ makeFileActive: true,
+ },
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
});
@@ -278,48 +316,84 @@ describe('IDE store file actions', () => {
let tmpFile;
beforeEach(() => {
- spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
+ spyOn(service, 'getRawFileData').and.callThrough();
tmpFile = file('tmpFile');
store.state.entries[tmpFile.path] = tmpFile;
});
- it('calls getRawFileData service method', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).replyOnce(200, 'raw');
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('calls getRawFileData service method', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
- it('updates file raw data', done => {
- store
- .dispatch('getRawFileData', { path: tmpFile.path })
- .then(() => {
- expect(tmpFile.raw).toBe('raw');
+ done();
+ })
+ .catch(done.fail);
+ });
- done();
- })
- .catch(done.fail);
- });
+ it('updates file raw data', done => {
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path })
+ .then(() => {
+ expect(tmpFile.raw).toBe('raw');
- it('calls also getBaseRawFileData service method', done => {
- spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+ done();
+ })
+ .catch(done.fail);
+ });
- tmpFile.mrChange = { new_file: false };
+ it('calls also getBaseRawFileData service method', done => {
+ spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
- store
- .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
- .then(() => {
- expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
- expect(tmpFile.baseRaw).toBe('baseraw');
+ tmpFile.mrChange = { new_file: false };
- done();
- })
- .catch(done.fail);
+ store
+ .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .then(() => {
+ expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+ expect(tmpFile.baseRaw).toBe('baseraw');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/(.*)/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ actions
+ .getRawFileData(
+ { state: store.state, commit() {}, dispatch },
+ { path: tmpFile.path, baseSha: tmpFile.baseSha },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the file content.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ path: tmpFile.path,
+ baseSha: tmpFile.baseSha,
+ },
+ });
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index b4ec4a0b173..c99ccc70c6a 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -1,110 +1,239 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
+import {
+ getMergeRequestData,
+ getMergeRequestChanges,
+ getMergeRequestVersions,
+} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('IDE store merge request actions', () => {
+ let mock;
+
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
store.state.projects.abcproject = {
mergeRequests: {},
};
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
describe('getMergeRequestData', () => {
- beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestData').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestData').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestData service method', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Object', done => {
+ store
+ .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+ expect(store.state.currentMergeRequestId).toBe(1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('calls getProjectMergeRequestData service method', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
- });
-
- it('sets the Merge Request Object', done => {
- store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
- expect(store.state.currentMergeRequestId).toBe(1);
-
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestChanges', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
- Promise.resolve({ data: { title: 'mergerequest' } }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
});
- it('calls getProjectMergeRequestChanges service method', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequestChanges').and.callThrough();
+
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
+ .reply(200, { title: 'mergerequest' });
+ });
+
+ it('calls getProjectMergeRequestChanges service method', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Changes Object', done => {
+ store
+ .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ 'mergerequest',
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Changes Object', done => {
- store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
- 'mergerequest',
- );
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestChanges(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request changes.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
describe('getMergeRequestVersions', () => {
beforeEach(() => {
- spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
- Promise.resolve({ data: [{ id: 789 }] }),
- );
-
store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
});
- it('calls getProjectMergeRequestVersions service method', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
-
- done();
- })
- .catch(done.fail);
+ describe('success', () => {
+ beforeEach(() => {
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
+ .reply(200, [{ id: 789 }]);
+ spyOn(service, 'getProjectMergeRequestVersions').and.callThrough();
+ });
+
+ it('calls getProjectMergeRequestVersions service method', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the Merge Request Versions Object', done => {
+ store
+ .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('sets the Merge Request Versions Object', done => {
- store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
- .then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
- done();
- })
- .catch(done.fail);
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError();
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatch');
+
+ getMergeRequestVersions(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ { projectId: 'abcproject', mergeRequestId: 1 },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading the merge request version data.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: {
+ projectId: 'abcproject',
+ mergeRequestId: 1,
+ force: false,
+ },
+ });
+
+ done();
+ });
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index d71fc0e035e..ca79edafb7e 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -1,15 +1,32 @@
-import { refreshLastCommitData } from '~/ide/stores/actions';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ refreshLastCommitData,
+ showBranchNotFoundError,
+ createNewBranchFromDefault,
+ getBranchData,
+} from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
+import api from '~/api';
+import router from '~/ide/ide_router';
import { resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
describe('IDE store project actions', () => {
+ let mock;
+
beforeEach(() => {
- store.state.projects['abc/def'] = {};
+ mock = new MockAdapter(axios);
+
+ store.state.projects['abc/def'] = {
+ branches: {},
+ };
});
afterEach(() => {
+ mock.restore();
+
resetStore(store);
});
@@ -80,4 +97,138 @@ describe('IDE store project actions', () => {
);
});
});
+
+ describe('showBranchNotFoundError', () => {
+ it('dispatches setErrorMessage', done => {
+ testAction(
+ showBranchNotFoundError,
+ 'master',
+ null,
+ [],
+ [
+ {
+ type: 'setErrorMessage',
+ payload: {
+ text: "Branch <strong>master</strong> was not found in this project's repository.",
+ action: jasmine.any(Function),
+ actionText: 'Create branch',
+ actionPayload: 'master',
+ },
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('createNewBranchFromDefault', () => {
+ it('calls API', done => {
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(api.createBranch).toHaveBeenCalledWith('project-path', {
+ ref: 'master',
+ branch: 'new-branch-name',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('clears error message', done => {
+ const dispatchSpy = jasmine.createSpy('dispatch');
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch: dispatchSpy,
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('reloads window', done => {
+ spyOn(api, 'createBranch').and.returnValue(Promise.resolve());
+ spyOn(router, 'push');
+
+ createNewBranchFromDefault(
+ {
+ state: {
+ currentProjectId: 'project-path',
+ },
+ getters: {
+ currentProject: {
+ default_branch: 'master',
+ },
+ },
+ dispatch() {},
+ },
+ 'new-branch-name',
+ )
+ .then(() => {
+ expect(router.push).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('getBranchData', () => {
+ describe('error', () => {
+ it('dispatches branch not found action when response is 404', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
+
+ mock.onGet(/(.*)/).replyOnce(404);
+
+ getBranchData(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch.calls.argsFor(0)).toEqual([
+ 'showBranchNotFoundError',
+ 'master-testing',
+ ]);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index e0ef57a3966..6860e6cdb91 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,11 +1,16 @@
-import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree';
+import * as types from '~/ide/stores/mutation_types';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
-import { file, resetStore } from '../../helpers';
+import { file, resetStore, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
let projectTree;
+ let mock;
const basicCallParameters = {
endpoint: 'rootEndpoint',
@@ -17,6 +22,8 @@ describe('Multi-file store tree actions', () => {
beforeEach(() => {
spyOn(router, 'push');
+ mock = new MockAdapter(axios);
+
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = {
@@ -30,49 +37,119 @@ describe('Multi-file store tree actions', () => {
});
afterEach(() => {
+ mock.restore();
resetStore(store);
});
describe('getFiles', () => {
- beforeEach(() => {
- spyOn(service, 'getFiles').and.returnValue(
- Promise.resolve({
- json: () =>
- Promise.resolve([
- 'file.txt',
- 'folder/fileinfolder.js',
- 'folder/subfolder/fileinsubfolder.js',
- ]),
- }),
- );
+ describe('success', () => {
+ beforeEach(() => {
+ spyOn(service, 'getFiles').and.callThrough();
+
+ mock
+ .onGet(/(.*)/)
+ .replyOnce(200, [
+ 'file.txt',
+ 'folder/fileinfolder.js',
+ 'folder/subfolder/fileinsubfolder.js',
+ ]);
+ });
+
+ it('calls service getFiles', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('adds data into tree', done => {
+ store
+ .dispatch('getFiles', basicCallParameters)
+ .then(() => {
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(2);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
+ expect(projectTree.tree[1].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
+ expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
+
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('calls service getFiles', done => {
- store
- .dispatch('getFiles', basicCallParameters)
- .then(() => {
- expect(service.getFiles).toHaveBeenCalledWith('', 'master');
+ describe('error', () => {
+ it('dispatches branch not found actions when response is 404', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
- done();
- })
- .catch(done.fail);
- });
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ },
+ };
- it('adds data into tree', done => {
- store
- .dispatch('getFiles', basicCallParameters)
- .then(() => {
- projectTree = store.state.trees['abcproject/master'];
- expect(projectTree.tree.length).toBe(2);
- expect(projectTree.tree[0].type).toBe('tree');
- expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
- expect(projectTree.tree[1].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
- expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
+ mock.onGet(/(.*)/).replyOnce(404);
- done();
- })
- .catch(done.fail);
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch.calls.argsFor(0)).toEqual([
+ 'showBranchNotFoundError',
+ 'master-testing',
+ ]);
+ done();
+ });
+ });
+
+ it('dispatches error action', done => {
+ const dispatch = jasmine.createSpy('dispatchSpy');
+
+ store.state.projects = {
+ 'abc/def': {
+ web_url: `${gl.TEST_HOST}/files`,
+ },
+ };
+
+ mock.onGet(/(.*)/).replyOnce(500);
+
+ getFiles(
+ {
+ commit() {},
+ dispatch,
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ )
+ .then(done.fail)
+ .catch(() => {
+ expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
+ text: 'An error occured whilst loading all the files.',
+ action: jasmine.any(Function),
+ actionText: 'Please try again',
+ actionPayload: { projectId: 'abc/def', branchId: 'master-testing' },
+ });
+ done();
+ });
+ });
});
});
@@ -96,71 +173,32 @@ describe('Multi-file store tree actions', () => {
});
});
- describe('getLastCommitData', () => {
+ describe('showTreeEntry', () => {
beforeEach(() => {
- spyOn(service, 'getTreeLastCommit').and.returnValue(
- Promise.resolve({
- headers: {
- 'more-logs-url': null,
- },
- json: () =>
- Promise.resolve([
- {
- type: 'tree',
- file_name: 'testing',
- commit: {
- message: 'commit message',
- authored_date: '123',
- },
- },
- ]),
- }),
- );
-
- store.state.trees['abcproject/mybranch'] = {
- tree: [],
- };
-
- projectTree = store.state.trees['abcproject/mybranch'];
- projectTree.tree.push(file('testing', '1', 'tree'));
- projectTree.lastCommitPath = 'lastcommitpath';
+ const paths = [
+ 'grandparent',
+ 'ancestor',
+ 'grandparent/parent',
+ 'grandparent/aunt',
+ 'grandparent/parent/child.txt',
+ 'grandparent/aunt/cousing.txt',
+ ];
+
+ Object.assign(store.state.entries, createEntriesFromPaths(paths));
});
- it('calls service with lastCommitPath', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('updates trees last commit data', done => {
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('does not update entry if not found', done => {
- projectTree.tree[0].name = 'a';
-
- store
- .dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
-
- done();
- })
- .catch(done.fail);
+ it('opens the parents', done => {
+ testAction(
+ showTreeEntry,
+ 'grandparent/parent/child.txt',
+ store.state,
+ [
+ { type: types.SET_TREE_OPEN, payload: 'grandparent/parent' },
+ { type: types.SET_TREE_OPEN, payload: 'grandparent' },
+ ],
+ [{ type: 'showTreeEntry' }],
+ done,
+ );
});
});
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 062c3497623..8b665a6d79e 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -6,6 +6,7 @@ import actions, {
setEmptyStateSvgs,
updateActivityBarView,
updateTempFlagForEntry,
+ setErrorMessage,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
@@ -443,4 +444,17 @@ describe('Multi-file store actions', () => {
);
});
});
+
+ describe('setErrorMessage', () => {
+ it('commis error messsage', done => {
+ testAction(
+ setErrorMessage,
+ 'error',
+ null,
+ [{ type: types.SET_ERROR_MESSAGE, payload: 'error' }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 4833ba3edfd..70883e16b0d 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -147,12 +147,11 @@ describe('IDE store getters', () => {
const commitTitle = 'Example commit title';
const localGetters = {
currentProject: {
- branches: {
- 'example-branch': {
- commit: {
- title: commitTitle,
- },
- },
+ name: 'test-project',
+ },
+ currentBranch: {
+ commit: {
+ title: commitTitle,
},
},
};
@@ -161,4 +160,23 @@ describe('IDE store getters', () => {
expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle);
});
});
+
+ describe('currentBranch', () => {
+ it('returns current projects branch', () => {
+ const localGetters = {
+ currentProject: {
+ branches: {
+ master: {
+ name: 'master',
+ },
+ },
+ },
+ };
+ localState.currentBranchId = 'master';
+
+ expect(getters.currentBranch(localState, localGetters)).toEqual({
+ name: 'master',
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index 55580f046ad..44c941d6dbb 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -29,46 +29,6 @@ describe('IDE commit module getters', () => {
});
});
- describe('commitButtonDisabled', () => {
- const localGetters = {
- discardDraftButtonDisabled: false,
- };
- const rootState = {
- stagedFiles: ['a'],
- };
-
- it('returns false when discardDraftButtonDisabled is false & stagedFiles is not empty', () => {
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeFalsy();
- });
-
- it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => {
- rootState.stagedFiles.length = 0;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
-
- it('returns true when discardDraftButtonDisabled is true', () => {
- localGetters.discardDraftButtonDisabled = true;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
-
- it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => {
- localGetters.discardDraftButtonDisabled = false;
- rootState.stagedFiles.length = 0;
-
- expect(
- getters.commitButtonDisabled(state, localGetters, rootState),
- ).toBeTruthy();
- });
- });
-
describe('newBranchName', () => {
it('includes username, currentBranchId, patch & random number', () => {
gon.current_username = 'username';
@@ -108,9 +68,7 @@ describe('IDE commit module getters', () => {
});
it('uses newBranchName when not empty', () => {
- expect(getters.branchName(state, localGetters, rootState)).toBe(
- 'state-newBranchName',
- );
+ expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName');
});
it('uses getters newBranchName when state newBranchName is empty', () => {
@@ -118,11 +76,53 @@ describe('IDE commit module getters', () => {
newBranchName: '',
});
- expect(getters.branchName(state, localGetters, rootState)).toBe(
- 'newBranchName',
- );
+ expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName');
});
});
});
});
+
+ describe('preBuiltCommitMessage', () => {
+ let rootState = {};
+
+ beforeEach(() => {
+ rootState.changedFiles = [];
+ rootState.stagedFiles = [];
+ });
+
+ afterEach(() => {
+ rootState = {};
+ });
+
+ it('returns commitMessage when set', () => {
+ state.commitMessage = 'test commit message';
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('test commit message');
+ });
+
+ ['changedFiles', 'stagedFiles'].forEach(key => {
+ it('returns commitMessage with updated file', () => {
+ rootState[key].push({
+ path: 'test-file',
+ });
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('Update test-file');
+ });
+
+ it('returns commitMessage with updated files', () => {
+ rootState[key].push(
+ {
+ path: 'test-file',
+ },
+ {
+ path: 'index.js',
+ },
+ );
+
+ expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe(
+ 'Update test-file, index.js files',
+ );
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 972713c5ad2..98016f593aa 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -148,4 +148,12 @@ describe('Multi-file store mutations', () => {
expect(localState.unusedSeal).toBe(false);
});
});
+
+ describe('SET_ERROR_MESSAGE', () => {
+ it('updates error message', () => {
+ mutations.SET_ERROR_MESSAGE(localState, 'error');
+
+ expect(localState.errorMessage).toBe('error');
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index a7bd443af51..6c5980cfae4 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -94,6 +94,7 @@ describe('Multi-file store utils', () => {
newBranch: false,
state,
rootState,
+ getters: {},
});
expect(payload).toEqual({
@@ -118,5 +119,58 @@ describe('Multi-file store utils', () => {
start_branch: undefined,
});
});
+
+ it('uses prebuilt commit message when commit message is empty', () => {
+ const rootState = {
+ stagedFiles: [
+ {
+ ...file('staged'),
+ path: 'staged',
+ content: 'updated file content',
+ lastCommitSha: '123456789',
+ },
+ {
+ ...file('newFile'),
+ path: 'added',
+ tempFile: true,
+ content: 'new file content',
+ base64: true,
+ lastCommitSha: '123456789',
+ },
+ ],
+ currentBranchId: 'master',
+ };
+ const payload = utils.createCommitPayload({
+ branch: 'master',
+ newBranch: false,
+ state: {},
+ rootState,
+ getters: {
+ preBuiltCommitMessage: 'prebuilt test commit message',
+ },
+ });
+
+ expect(payload).toEqual({
+ branch: 'master',
+ commit_message: 'prebuilt test commit message',
+ actions: [
+ {
+ action: 'update',
+ file_path: 'staged',
+ content: 'updated file content',
+ encoding: 'text',
+ last_commit_id: '123456789',
+ },
+ {
+ action: 'create',
+ file_path: 'added',
+ content: 'new file content',
+ encoding: 'base64',
+ last_commit_id: '123456789',
+ },
+ ],
+ start_branch: undefined,
+ });
+ });
});
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 22eb0ad7143..7502f1fa2e1 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -19,9 +19,11 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
- mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+ mock
+ .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`)
+ .reply(200, {});
- return this.merge = new MergeRequest();
+ return (this.merge = new MergeRequest());
});
afterEach(() => {
@@ -32,17 +34,22 @@ import IssuablesHelper from '~/helpers/issuables_helper';
spyOn($, 'ajax').and.stub();
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
- $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent);
+ $('input[type=checkbox]')
+ .attr('checked', true)[0]
+ .dispatchEvent(changeEvent);
return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', (done) => {
+ it('submits an ajax request on tasklist:changed', done => {
$('.js-task-list-field').trigger('tasklist:changed');
setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
- merge_request: { description: '- [ ] Task List Item' },
- });
+ expect(axios.patch).toHaveBeenCalledWith(
+ `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`,
+ {
+ merge_request: { description: '- [ ] Task List Item' },
+ },
+ );
done();
});
});
@@ -119,4 +126,4 @@ import IssuablesHelper from '~/helpers/issuables_helper';
});
});
});
-}).call(window);
+}.call(window));
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 08928e13985..7251ce19a90 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -40,6 +40,7 @@ describe('MergeRequestTabs', function() {
this.class.unbindEvents();
this.class.destroyPipelinesView();
mrPageMock.restore();
+ $('.js-merge-request-test').remove();
});
describe('opensInNewTab', function() {
diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js
index 3b2641f7646..07b82ce721e 100644
--- a/spec/javascripts/namespace_select_spec.js
+++ b/spec/javascripts/namespace_select_spec.js
@@ -22,7 +22,7 @@ describe('NamespaceSelect', () => {
const dropdown = document.createElement('div');
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('prevents click events', () => {
@@ -43,7 +43,7 @@ describe('NamespaceSelect', () => {
dropdown.dataset.isFilter = 'true';
// eslint-disable-next-line no-new
new NamespaceSelect({ dropdown });
- glDropdownOptions = $.fn.glDropdown.calls.argsFor(0)[0];
+ [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0);
});
it('does not prevent click events', () => {
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
index 8f8ba231ae8..0b1b11de1fd 100644
--- a/spec/javascripts/notebook/cells/markdown_spec.js
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -14,6 +14,7 @@ describe('Markdown component', () => {
beforeEach((done) => {
json = getJSONFixture('blob/notebook/basic.json');
+ // eslint-disable-next-line prefer-destructuring
cell = json.cells[1];
vm = new Component({
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 985c2f81ef3..71ef3aa9b03 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -291,4 +291,17 @@ describe('Actions Notes Store', () => {
.catch(done.fail);
});
});
+
+ describe('setNotesFetchedState', () => {
+ it('should set notes fetched state', done => {
+ testAction(
+ actions.setNotesFetchedState,
+ true,
+ {},
+ [{ type: 'SET_NOTES_FETCHED_STATE', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 5501e50e97b..815cc09621f 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -15,6 +15,7 @@ describe('Getters Notes Store', () => {
discussions: [individualNote],
targetNoteHash: 'hash',
lastFetchedAt: 'timestamp',
+ isNotesFetched: false,
notesData: notesDataMock,
userData: userDataMock,
@@ -84,4 +85,10 @@ describe('Getters Notes Store', () => {
expect(getters.openState(state)).toEqual(noteableDataMock.state);
});
});
+
+ describe('isNotesFetched', () => {
+ it('should return the state for the fetching notes', () => {
+ expect(getters.isNotesFetched(state)).toBeFalsy();
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 556a1c244c0..ccc7328447b 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -318,4 +318,15 @@ describe('Notes Store mutations', () => {
expect(state.isToggleStateButtonLoading).toEqual(false);
});
});
+
+ describe('SET_NOTES_FETCHING_STATE', () => {
+ it('should set the given state', () => {
+ const state = {
+ isNotesFetched: false,
+ };
+
+ mutations.SET_NOTES_FETCHED_STATE(state, true);
+ expect(state.isNotesFetched).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index 073dae56c25..9c55a19ebc7 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -135,4 +135,34 @@ describe('pipeline graph job component', () => {
expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success');
});
});
+
+ describe('tooltip placement', () => {
+ const tooltipBoundary = 'a[data-boundary="viewport"]';
+
+ it('does not set tooltip boundary by default', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+
+ it('sets tooltip boundary to viewport for small dropdowns', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 1,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).not.toBeNull();
+ });
+
+ it('does not set tooltip boundary for large lists', () => {
+ component = mountComponent(JobComponent, {
+ job: mockJob,
+ dropdownLength: 7,
+ });
+
+ expect(component.$el.querySelector(tooltipBoundary)).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 78d8e9e572e..03ffc122795 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -24,7 +24,7 @@ describe('Pipelines Table Row', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null);
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 4fc3c08145e..d21ba35e96d 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -11,7 +11,7 @@ describe('Pipelines Table', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- const pipelines = getJSONFixture(jsonFixtureName).pipelines;
+ const { pipelines } = getJSONFixture(jsonFixtureName);
PipelinesTableComponent = Vue.extend(pipelinesTableComp);
pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
index a54219d58c2..60153672214 100644
--- a/spec/javascripts/smart_interval_spec.js
+++ b/spec/javascripts/smart_interval_spec.js
@@ -87,7 +87,7 @@ describe('SmartInterval', function () {
setTimeout(() => {
interval.cancel();
- const intervalId = interval.state.intervalId;
+ const { intervalId } = interval.state;
const currentInterval = interval.getCurrentInterval();
const intervalLowerLimit = interval.cfg.startingInterval;
@@ -106,7 +106,7 @@ describe('SmartInterval', function () {
interval.resume();
- const intervalId = interval.state.intervalId;
+ const { intervalId } = interval.state;
expect(intervalId).toBeTruthy();
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index aeb936b0e3c..0eff98bcc9d 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -3,7 +3,6 @@
import $ from 'jquery';
import 'vendor/jasmine-jquery';
import '~/commons';
-
import Vue from 'vue';
import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate';
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index d84b13b07c4..57e0caa692c 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -6,7 +6,7 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', function () {
preloadFixtures('u2f/authenticate.html.raw');
- beforeEach((done) => {
+ beforeEach(() => {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
@@ -19,46 +19,70 @@ describe('U2FAuthenticate', function () {
document.querySelector('#js-login-2fa-device'),
document.querySelector('.js-2fa-form'),
);
+ });
- // bypass automatic form submission within renderAuthenticated
- spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ describe('with u2f unavailable', () => {
+ beforeEach(() => {
+ spyOn(this.component, 'switchToFallbackUI');
+ this.oldu2f = window.u2f;
+ window.u2f = null;
+ });
- this.component.start().then(done).catch(done.fail);
- });
+ afterEach(() => {
+ window.u2f = this.oldu2f;
+ });
- it('allows authenticating via a U2F device', () => {
- const inProgressMessage = this.container.find('p');
- expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
- this.u2fDevice.respondToAuthenticateRequest({
- deviceData: 'this is data from the device',
+ it('falls back to normal 2fa', (done) => {
+ this.component.start().then(() => {
+ expect(this.component.switchToFallbackUI).toHaveBeenCalled();
+ done();
+ }).catch(done.fail);
});
- expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
- describe('errors', () => {
- it('displays an error message', () => {
- const setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const errorMessage = this.container.find('p');
- return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ describe('with u2f available', () => {
+ beforeEach((done) => {
+ // bypass automatic form submission within renderAuthenticated
+ spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
+ this.u2fDevice = new MockU2FDevice();
+
+ this.component.start().then(done).catch(done.fail);
});
- return it('allows retrying authentication after an error', () => {
- let setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
- this.u2fDevice.respondToAuthenticateRequest({
- errorCode: 'error!',
- });
- const retryButton = this.container.find('#js-u2f-try-again');
- retryButton.trigger('click');
- setupButton = this.container.find('#js-login-u2f-device');
- setupButton.trigger('click');
+
+ it('allows authenticating via a U2F device', () => {
+ const inProgressMessage = this.container.find('p');
+ expect(inProgressMessage.text()).toContain('Trying to communicate with your device');
this.u2fDevice.respondToAuthenticateRequest({
deviceData: 'this is data from the device',
});
expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
+
+ describe('errors', () => {
+ it('displays an error message', () => {
+ const setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
+ });
+ const errorMessage = this.container.find('p');
+ return expect(errorMessage.text()).toContain('There was a problem communicating with your device');
+ });
+ return it('allows retrying authentication after an error', () => {
+ let setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: 'error!',
+ });
+ const retryButton = this.container.find('#js-u2f-try-again');
+ retryButton.trigger('click');
+ setupButton = this.container.find('#js-login-u2f-device');
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: 'this is data from the device',
+ });
+ expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
index f7581251bf0..1c666fc6c55 100644
--- a/spec/javascripts/vue_shared/components/file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -74,7 +74,7 @@ describe('File Icon component', () => {
size: 120,
});
- const classList = vm.$el.firstChild.classList;
+ const { classList } = vm.$el.firstChild;
const containsSizeClass = classList.contains('s120');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index 68d57ebc8f0..cc030e29d61 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -44,7 +44,7 @@ describe('Sprite Icon Component', function () {
});
it('should properly render img css', function () {
- const classList = icon.$el.classList;
+ const { classList } = icon.$el;
const containsSizeClass = classList.contains('s32');
const containsCustomClass = classList.contains('extraclasses');
expect(containsSizeClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 446f025c127..656b57d764e 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -51,7 +51,7 @@ describe('User Avatar Image Component', function () {
});
it('should properly render img css', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,7 +73,7 @@ describe('User Avatar Image Component', function () {
});
it('should add lazy attributes', function () {
- const classList = vm.$el.classList;
+ const { classList } = vm.$el;
const lazyClass = classList.contains('lazy');
expect(lazyClass).toBe(true);
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index adf80d0c2bb..4c5c242cbb3 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -21,7 +21,7 @@ describe('User Avatar Link Component', function () {
propsData: this.propsData,
}).$mount();
- this.userAvatarImage = this.userAvatarLink.$children[0];
+ [this.userAvatarImage] = this.userAvatarLink.$children;
});
it('should return a defined Vue component', function () {
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index 10910f22d4a..85a4619e33d 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -3,15 +3,6 @@ require 'spec_helper'
describe Banzai::Filter::EmojiFilter do
include FilterSpecHelper
- before do
- @original_asset_host = ActionController::Base.asset_host
- ActionController::Base.asset_host = 'https://foo.com'
- end
-
- after do
- ActionController::Base.asset_host = @original_asset_host
- end
-
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
expect(doc.css('gl-emoji').first.text).to eq '❤'
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 17a620ef603..d930c608b18 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -93,6 +93,16 @@ describe Banzai::Filter::SanitizationFilter do
expect(doc.at_css('td')['style']).to eq 'text-align: center'
end
+ it 'disallows `text-align` property in `style` attribute on other elements' do
+ html = <<~HTML
+ <div style="text-align: center">Text</div>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('div')['style']).to be_nil
+ end
+
it 'allows `span` elements' do
exp = act = %q{<span>Hello</span>}
expect(filter(act).to_html).to eq exp
@@ -224,7 +234,7 @@ describe Banzai::Filter::SanitizationFilter do
'protocol-based JS injection: spaces and entities' => {
input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
- output: '<a href="">foo</a>'
+ output: '<a href>foo</a>'
},
'protocol whitespace' => {
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 0cfef4ff5bf..7213cd58ea7 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -139,5 +139,14 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(items[5].ancestors).to include(items[4])
end
end
+
+ context 'header text contains escaped content' do
+ let(:content) { '&lt;img src="x" onerror="alert(42)"&gt;' }
+ let(:results) { result(header(1, content)) }
+
+ it 'outputs escaped content' do
+ expect(doc.inner_html).to include(content)
+ end
+ end
end
end
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
index ed5d56e91d4..09bf21b5946 100644
--- a/spec/lib/gitaly/server_spec.rb
+++ b/spec/lib/gitaly/server_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitaly::Server do
+ let(:server) { described_class.new('default') }
+
describe '.all' do
let(:storages) { Gitlab.config.repositories.storages }
@@ -17,6 +19,38 @@ describe Gitaly::Server do
it { is_expected.to respond_to(:up_to_date?) }
it { is_expected.to respond_to(:address) }
+ describe 'readable?' do
+ context 'when the storage is readable' do
+ it 'returns true' do
+ expect(server).to be_readable
+ end
+ end
+
+ context 'when the storage is not readable' do
+ let(:server) { described_class.new('broken') }
+
+ it 'returns false' do
+ expect(server).not_to be_readable
+ end
+ end
+ end
+
+ describe 'writeable?' do
+ context 'when the storage is writeable' do
+ it 'returns true' do
+ expect(server).to be_writeable
+ end
+ end
+
+ context 'when the storage is not writeable' do
+ let(:server) { described_class.new('broken') }
+
+ it 'returns false' do
+ expect(server).not_to be_writeable
+ end
+ end
+ end
+
describe 'request memoization' do
context 'when requesting multiple properties', :request_store do
it 'uses memoization for the info request' do
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 64f3d09a25b..3a8667e434d 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -779,4 +779,12 @@ describe Gitlab::Auth::OAuth::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ subject { oauth_user.bypass_two_factor? }
+
+ it 'returns always false' do
+ is_expected.to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index bb950e6bbf8..76f49e778fb 100644
--- a/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -37,4 +37,55 @@ describe Gitlab::Auth::Saml::AuthHash do
end
end
end
+
+ describe '#authn_context' do
+ let(:auth_hash_data) do
+ {
+ provider: 'saml',
+ uid: 'some_uid',
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials:
+ {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra:
+ {
+ raw_info:
+ {
+ info:
+ {
+ name: 'mockuser',
+ email: 'mock@email.ch',
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ }
+ end
+
+ subject(:saml_auth_hash) { described_class.new(OmniAuth::AuthHash.new(auth_hash_data)) }
+
+ context 'with response_object' do
+ before do
+ auth_hash_data[:extra][:response_object] = { document:
+ saml_xml(File.read('spec/fixtures/authentication/saml_response.xml')) }
+ end
+
+ it 'can extract authn_context' do
+ expect(saml_auth_hash.authn_context).to eq 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ end
+ end
+
+ context 'without response_object' do
+ it 'returns an empty string' do
+ expect(saml_auth_hash.authn_context).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/auth/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index 62514ca0688..c523f5e177f 100644
--- a/spec/lib/gitlab/auth/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -400,4 +400,45 @@ describe Gitlab::Auth::Saml::User do
end
end
end
+
+ describe '#bypass_two_factor?' do
+ let(:saml_config) { mock_saml_config_with_upstream_two_factor_authn_contexts }
+
+ subject { saml_user.bypass_two_factor? }
+
+ context 'with authn_contexts_worth_two_factors configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
+ end
+
+ it 'returns true when authn_context is worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when authn_context is not worth two factors' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:Password')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'without auth_contexts_worth_two_factors_configured' do
+ before do
+ stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [mock_saml_config])
+ end
+
+ it 'returns false when authn_context is present' do
+ allow(saml_user.auth_hash).to receive(:authn_context).and_return('urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS')
+ is_expected.to be_falsey
+ end
+
+ it 'returns false when authn_context is blank' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
new file mode 100644
index 00000000000..a251ab323d8
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, schema: 20180619121030 do
+ describe '#perform' do
+ context 'when diff files can be deleted' do
+ let(:merge_request) { create(:merge_request, :merged) }
+ let(:merge_request_diff) do
+ merge_request.create_merge_request_diff
+ merge_request.merge_request_diffs.first
+ end
+
+ it 'deletes all merge request diff files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20).to(0)
+ end
+
+ it 'updates state to without_files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.reload.state }
+ .from('collected').to('without_files')
+ end
+
+ it 'rollsback if something goes wrong' do
+ expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all)
+ .and_raise
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to raise_error
+
+ merge_request_diff.reload
+
+ expect(merge_request_diff.state).to eq('collected')
+ expect(merge_request_diff.merge_request_diff_files.count).to eq(20)
+ end
+ end
+
+ it 'deletes no merge request diff files when MR is not merged' do
+ merge_request = create(:merge_request, :opened)
+ merge_request.create_merge_request_diff
+ merge_request_diff = merge_request.merge_request_diffs.first
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .not_to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20)
+ end
+
+ it 'deletes no merge request diff files when diff is marked as "without_files"' do
+ merge_request = create(:merge_request, :merged)
+ merge_request.create_merge_request_diff
+ merge_request_diff = merge_request.merge_request_diffs.first
+
+ merge_request_diff.clean!
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .not_to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20)
+ end
+
+ it 'deletes no merge request diff files when diff is the latest' do
+ merge_request = create(:merge_request, :merged)
+ merge_request_diff = merge_request.merge_request_diff
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .not_to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index e79f0a7f257..adb3ff4321f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -1,19 +1,69 @@
require 'spec_helper'
describe Gitlab::Ci::Variables::Collection::Item do
+ let(:variable_key) { 'VAR' }
+ let(:variable_value) { 'something' }
+ let(:expected_value) { variable_value }
+
let(:variable) do
- { key: 'VAR', value: 'something', public: true }
+ { key: variable_key, value: variable_value, public: true }
end
describe '.new' do
- it 'raises error if unknown key i specified' do
- expect { described_class.new(key: 'VAR', value: 'abc', files: true) }
- .to raise_error ArgumentError, 'unknown keyword: files'
+ context 'when unknown keyword is specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key, value: 'abc', files: true) }
+ .to raise_error ArgumentError, 'unknown keyword: files'
+ end
+ end
+
+ context 'when required keywords are not specified' do
+ it 'raises error' do
+ expect { described_class.new(key: variable_key) }
+ .to raise_error ArgumentError, 'missing keyword: value'
+ end
end
- it 'raises error when required keywords are not specified' do
- expect { described_class.new(key: 'VAR') }
- .to raise_error ArgumentError, 'missing keyword: value'
+ shared_examples 'creates variable' do
+ subject { described_class.new(key: variable_key, value: variable_value) }
+
+ it 'saves given value' do
+ expect(subject[:key]).to eq variable_key
+ expect(subject[:value]).to eq expected_value
+ end
+ end
+
+ shared_examples 'raises error for invalid type' do
+ it do
+ expect { described_class.new(key: variable_key, value: variable_value) }
+ .to raise_error ArgumentError, /`value` must be of type String, while it was:/
+ end
+ end
+
+ it_behaves_like 'creates variable'
+
+ context "when it's nil" do
+ let(:variable_value) { nil }
+ let(:expected_value) { nil }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context "when it's an empty string" do
+ let(:variable_value) { '' }
+ let(:expected_value) { '' }
+
+ it_behaves_like 'creates variable'
+ end
+
+ context 'when provided value is not a string' do
+ [1, false, [], {}, Object.new].each do |val|
+ context "when it's #{val}" do
+ let(:variable_value) { val }
+
+ it_behaves_like 'raises error for invalid type'
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index cb2f7718c9c..5c91816a586 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Ci::Variables::Collection do
end
it 'appends an internal resource' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
subject.append(collection.first)
@@ -74,15 +74,15 @@ describe Gitlab::Ci::Variables::Collection do
describe '#+' do
it 'makes it possible to combine with an array' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
variables = [{ key: 'TEST', value: 'something' }]
expect((collection + variables).count).to eq 2
end
it 'makes it possible to combine with another collection' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
- other = described_class.new([{ key: 'TEST', value: 2 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
+ other = described_class.new([{ key: 'TEST', value: '2' }])
expect((collection + other).count).to eq 2
end
@@ -90,10 +90,10 @@ describe Gitlab::Ci::Variables::Collection do
describe '#to_runner_variables' do
it 'creates an array of hashes in a runner-compatible format' do
- collection = described_class.new([{ key: 'TEST', value: 1 }])
+ collection = described_class.new([{ key: 'TEST', value: '1' }])
expect(collection.to_runner_variables)
- .to eq [{ key: 'TEST', value: 1, public: true }]
+ .to eq [{ key: 'TEST', value: '1', public: true }]
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index fa5327c26f0..e73cdc54a15 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -5,16 +5,6 @@ module Gitlab
describe YamlProcessor do
subject { described_class.new(config) }
- describe 'our current .gitlab-ci.yml' do
- let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
-
- it 'is valid' do
- error_message = described_class.validation_message(config)
-
- expect(error_message).to be_nil
- end
- end
-
describe '#build_attributes' do
subject { described_class.new(config).build_attributes(:rspec) }
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
index 4f8412108ba..b236c1a9c49 100644
--- a/spec/lib/gitlab/data_builder/note_spec.rb
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -52,7 +52,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:issue].except('updated_at'))
.to eq(issue.reload.hook_attrs.except('updated_at'))
expect(data[:issue]['updated_at'])
- .to be > issue.hook_attrs['updated_at']
+ .to be >= issue.hook_attrs['updated_at']
end
context 'with confidential issue' do
@@ -84,7 +84,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:merge_request].except('updated_at'))
.to eq(merge_request.reload.hook_attrs.except('updated_at'))
expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
+ .to be >= merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -107,7 +107,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:merge_request].except('updated_at'))
.to eq(merge_request.reload.hook_attrs.except('updated_at'))
expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
+ .to be >= merge_request.hook_attrs['updated_at']
end
include_examples 'project hook data'
@@ -130,7 +130,7 @@ describe Gitlab::DataBuilder::Note do
expect(data[:snippet].except('updated_at'))
.to eq(snippet.reload.hook_attrs.except('updated_at'))
expect(data[:snippet]['updated_at'])
- .to be > snippet.hook_attrs['updated_at']
+ .to be >= snippet.hook_attrs['updated_at']
end
include_examples 'project hook data'
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 280f799f2ab..eb7148ff108 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1178,6 +1178,61 @@ describe Gitlab::Database::MigrationHelpers do
end
end
+ describe '#rename_column_using_background_migration' do
+ let!(:issue) { create(:issue, :closed, closed_at: Time.zone.now) }
+
+ it 'renames a column using a background migration' do
+ expect(model)
+ .to receive(:add_column)
+ .with(
+ 'issues',
+ :closed_at_timestamp,
+ :datetime_with_timezone,
+ limit: anything,
+ precision: anything,
+ scale: anything
+ )
+
+ expect(model)
+ .to receive(:install_rename_triggers)
+ .with('issues', :closed_at, :closed_at_timestamp)
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 10.minutes,
+ 'CopyColumn',
+ ['issues', :closed_at, :closed_at_timestamp, issue.id, issue.id]
+ )
+
+ expect(BackgroundMigrationWorker)
+ .to receive(:perform_in)
+ .ordered
+ .with(
+ 1.hour + 10.minutes,
+ 'CleanupConcurrentRename',
+ ['issues', :closed_at, :closed_at_timestamp]
+ )
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CopyColumn')
+
+ expect(Gitlab::BackgroundMigration)
+ .to receive(:steal)
+ .ordered
+ .with('CleanupConcurrentRename')
+
+ model.rename_column_using_background_migration(
+ 'issues',
+ :closed_at,
+ :closed_at_timestamp
+ )
+ end
+ end
+
describe '#perform_background_migration_inline?' do
it 'returns true in a test environment' do
allow(Rails.env)
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 122dcd9634c..68abcb3520a 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
end
it 'returns a full url when the asset host is configured' do
- allow(Gitlab::Application.config).to receive(:asset_host).and_return('http://assets.local')
+ allow(ActionController::Base).to receive(:asset_host).and_return('http://assets.local')
expect(described_class.main).to match %r{^http://localhost/assets/favicon-(?:\h+).png$}
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 6015086f002..b6061df349d 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- shared_examples 'finding blobs' do
+ describe '.find' do
context 'nil path' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
@@ -125,16 +125,6 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe '.find' do
- context 'when project_raw_show Gitaly feature is enabled' do
- it_behaves_like 'finding blobs'
- end
-
- context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding blobs'
- end
- end
-
shared_examples 'finding blobs by ID' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index ae69a362dda..ee74c2769eb 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -309,7 +309,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
- shared_examples '.shas_with_signatures' do
+ describe '.shas_with_signatures' do
let(:signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e 570e7b2abdd848b95f2f578043fc23bd6f6fd24d] }
let(:unsigned_shas) { %w[19e2e9b4ef76b422ce1154af39a91323ccc57434 c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
let(:first_signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
@@ -330,93 +330,55 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '.shas_with_signatures with gitaly on' do
- it_should_behave_like '.shas_with_signatures'
- end
-
- describe '.shas_with_signatures with gitaly disabled', :disable_gitaly do
- it_should_behave_like '.shas_with_signatures'
- end
-
describe '.find_all' do
- shared_examples 'finding all commits' do
- it 'should return a return a collection of commits' do
- commits = described_class.find_all(repository)
-
- expect(commits).to all( be_a_kind_of(described_class) )
- end
-
- context 'max_count' do
- subject do
- commits = described_class.find_all(
- repository,
- max_count: 50
- )
+ it 'should return a return a collection of commits' do
+ commits = described_class.find_all(repository)
- commits.map(&:id)
- end
+ expect(commits).to all( be_a_kind_of(described_class) )
+ end
- it 'has 34 elements' do
- expect(subject.size).to eq(34)
- end
+ context 'max_count' do
+ subject do
+ commits = described_class.find_all(
+ repository,
+ max_count: 50
+ )
- it 'includes the expected commits' do
- expect(subject).to include(
- SeedRepo::Commit::ID,
- SeedRepo::Commit::PARENT_ID,
- SeedRepo::FirstCommit::ID
- )
- end
+ commits.map(&:id)
end
- context 'ref + max_count + skip' do
- subject do
- commits = described_class.find_all(
- repository,
- ref: 'master',
- max_count: 50,
- skip: 1
- )
-
- commits.map(&:id)
- end
-
- it 'has 24 elements' do
- expect(subject.size).to eq(24)
- end
-
- it 'includes the expected commits' do
- expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID)
- expect(subject).not_to include(SeedRepo::LastCommit::ID)
- end
+ it 'has 34 elements' do
+ expect(subject.size).to eq(34)
end
- end
- context 'when Gitaly find_all_commits feature is enabled' do
- it_behaves_like 'finding all commits'
+ it 'includes the expected commits' do
+ expect(subject).to include(
+ SeedRepo::Commit::ID,
+ SeedRepo::Commit::PARENT_ID,
+ SeedRepo::FirstCommit::ID
+ )
+ end
end
- context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do
- it_behaves_like 'finding all commits'
-
- context 'while applying a sort order based on the `order` option' do
- it "allows ordering topologically (no parents shown before their children)" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO)
-
- described_class.find_all(repository, order: :topo)
- end
-
- it "allows ordering by date" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO)
+ context 'ref + max_count + skip' do
+ subject do
+ commits = described_class.find_all(
+ repository,
+ ref: 'master',
+ max_count: 50,
+ skip: 1
+ )
- described_class.find_all(repository, order: :date)
- end
+ commits.map(&:id)
+ end
- it "applies no sorting by default" do
- expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE)
+ it 'has 24 elements' do
+ expect(subject.size).to eq(24)
+ end
- described_class.find_all(repository)
- end
+ it 'includes the expected commits' do
+ expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID)
+ expect(subject).not_to include(SeedRepo::LastCommit::ID)
end
end
end
@@ -498,7 +460,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '.extract_signature_lazily' do
- shared_examples 'loading signatures in batch once' do
+ describe 'loading signatures in batch once' do
it 'fetches signatures in batch once' do
commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6]
signatures = commit_ids.map do |commit_id|
@@ -516,27 +478,13 @@ describe Gitlab::Git::Commit, seed_helper: true do
subject { described_class.extract_signature_lazily(repository, commit_id).itself }
- context 'with Gitaly extract_commit_signature_in_batch feature enabled' do
- it_behaves_like 'extracting commit signature'
- it_behaves_like 'loading signatures in batch once'
- end
-
- context 'with Gitaly extract_commit_signature_in_batch feature disabled', :disable_gitaly do
- it_behaves_like 'extracting commit signature'
- it_behaves_like 'loading signatures in batch once'
- end
+ it_behaves_like 'extracting commit signature'
end
describe '.extract_signature' do
subject { described_class.extract_signature(repository, commit_id) }
- context 'with gitaly' do
- it_behaves_like 'extracting commit signature'
- end
-
- context 'without gitaly', :disable_gitaly do
- it_behaves_like 'extracting commit signature'
- end
+ it_behaves_like 'extracting commit signature'
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 45f0006dc85..6ec4b90d70c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -996,46 +996,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#rugged_commits_between" do
- around do |example|
- # TODO #rugged_commits_between will be removed, has been migrated to gitaly
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- example.run
- end
- end
-
- context 'two SHAs' do
- let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
- let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
-
- it 'returns the number of commits between' do
- expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3)
- end
- end
-
- context 'SHA and master branch' do
- let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
- let(:branch) { 'master' }
-
- it 'returns the number of commits between a sha and a branch' do
- expect(repository.rugged_commits_between(sha, branch).count).to eq(5)
- end
-
- it 'returns the number of commits between a branch and a sha' do
- expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch
- end
- end
-
- context 'two branches' do
- let(:first_branch) { 'feature' }
- let(:second_branch) { 'master' }
-
- it 'returns the number of commits between' do
- expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17)
- end
- end
- end
-
describe '#count_commits_between' do
subject { repository.count_commits_between('feature', 'master') }
@@ -1871,49 +1831,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
repository_rugged.config["gitlab.fullpath"] = repository_path
end
- shared_examples 'writing repo config' do
- context 'is given a path' do
- it 'writes it to disk' do
- repository.write_config(full_path: "not-the/real-path.git")
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.write_config(full_path: "not-the/real-path.git")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = not-the/real-path.git")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
end
+ end
- context 'it is given an empty path' do
- it 'does not write it to disk' do
- repository.write_config(full_path: "")
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.write_config(full_path: "")
- config = File.read(File.join(repository_path, "config"))
+ config = File.read(File.join(repository_path, "config"))
- expect(config).to include("[gitlab]")
- expect(config).to include("fullpath = #{repository_path}")
- end
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository_path}")
end
+ end
- context 'repository does not exist' do
- it 'raises NoRepository and does not call Gitaly WriteConfig' do
- repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
+ context 'repository does not exist' do
+ it 'raises NoRepository and does not call Gitaly WriteConfig' do
+ repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
- expect(repository.gitaly_repository_client).not_to receive(:write_config)
+ expect(repository.gitaly_repository_client).not_to receive(:write_config)
- expect do
- repository.write_config(full_path: 'foo/bar.git')
- end.to raise_error(Gitlab::Git::Repository::NoRepository)
- end
+ expect do
+ repository.write_config(full_path: 'foo/bar.git')
+ end.to raise_error(Gitlab::Git::Repository::NoRepository)
end
end
-
- context "when gitaly_write_config is enabled" do
- it_behaves_like "writing repo config"
- end
-
- context "when gitaly_write_config is disabled", :disable_gitaly do
- it_behaves_like "writing repo config"
- end
end
describe '#merge' do
@@ -2160,43 +2110,33 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#create_from_bundle' do
- shared_examples 'creating repo from bundle' do
- let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
- let(:project) { create(:project) }
- let(:imported_repo) { project.repository.raw }
-
- before do
- expect(repository.bundle_to_disk(bundle_path)).to be true
- end
+ let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+ let(:project) { create(:project) }
+ let(:imported_repo) { project.repository.raw }
- after do
- FileUtils.rm_rf(bundle_path)
- end
-
- it 'creates a repo from a bundle file' do
- expect(imported_repo).not_to exist
+ before do
+ expect(repository.bundle_to_disk(bundle_path)).to be_truthy
+ end
- result = imported_repo.create_from_bundle(bundle_path)
+ after do
+ FileUtils.rm_rf(bundle_path)
+ end
- expect(result).to be true
- expect(imported_repo).to exist
- expect { imported_repo.fsck }.not_to raise_exception
- end
+ it 'creates a repo from a bundle file' do
+ expect(imported_repo).not_to exist
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(bundle_path)
- hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
+ result = imported_repo.create_from_bundle(bundle_path)
- expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
+ expect(result).to be_truthy
+ expect(imported_repo).to exist
+ expect { imported_repo.fsck }.not_to raise_exception
end
- context 'when Gitaly create_repo_from_bundle feature is enabled' do
- it_behaves_like 'creating repo from bundle'
- end
+ it 'creates a symlink to the global hooks dir' do
+ imported_repo.create_from_bundle(bundle_path)
+ hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
- context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do
- it_behaves_like 'creating repo from bundle'
+ expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index 44695acbe7d..51fad6c6838 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -164,7 +164,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
Timecop.freeze do
importer.update_repository
- expect(project.last_repository_updated_at).to eq(Time.zone.now)
+ expect(project.last_repository_updated_at).to be_like_time(Time.zone.now)
end
end
end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
deleted file mode 100644
index 9dcf272d25e..00000000000
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ /dev/null
@@ -1,200 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::HealthChecks::FsShardsCheck do
- def command_exists?(command)
- _, status = Gitlab::Popen.popen(%W{ #{command} 1 echo })
- status.zero?
- rescue Errno::ENOENT
- false
- end
-
- def timeout_command
- @timeout_command ||=
- if command_exists?('timeout')
- 'timeout'
- elsif command_exists?('gtimeout')
- 'gtimeout'
- else
- ''
- end
- end
-
- let(:metric_class) { Gitlab::HealthChecks::Metric }
- let(:result_class) { Gitlab::HealthChecks::Result }
- let(:repository_storages) { ['default'] }
- let(:tmp_dir) { Dir.mktmpdir }
-
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => tmp_dir)
- }.with_indifferent_access
- end
-
- before do
- allow(described_class).to receive(:repository_storages) { repository_storages }
- allow(described_class).to receive(:storages_paths) { storages_paths }
- stub_const('Gitlab::HealthChecks::FsShardsCheck::TIMEOUT_EXECUTABLE', timeout_command)
- end
-
- after do
- FileUtils.remove_entry_secure(tmp_dir) if Dir.exist?(tmp_dir)
- end
-
- shared_examples 'filesystem checks' do
- describe '#readiness' do
- subject { described_class.readiness }
-
- context 'storage has a tripped circuitbreaker', :broken_storage do
- let(:repository_storages) { ['broken'] }
- let(:storages_paths) do
- Gitlab.config.repositories.storages
- end
-
- it { is_expected.to include(result_class.new(false, 'circuitbreaker tripped', shard: 'broken')) }
- end
-
- context 'storage points to not existing folder' do
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
- }.with_indifferent_access
- end
-
- before do
- allow(described_class).to receive(:storage_circuitbreaker_test) { true }
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
- end
-
- context 'storage points to directory that has both read and write rights' do
- before do
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- it { is_expected.to include(result_class.new(true, nil, shard: 'default')) }
-
- it 'cleans up files used for testing' do
- expect(described_class).to receive(:storage_write_test).with(any_args).and_call_original
-
- expect { subject }.not_to change(Dir.entries(tmp_dir), :count)
- end
-
- context 'read test fails' do
- before do
- allow(described_class).to receive(:storage_read_test).with(any_args).and_return(false)
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot read from storage', shard: 'default')) }
- end
-
- context 'write test fails' do
- before do
- allow(described_class).to receive(:storage_write_test).with(any_args).and_return(false)
- end
-
- it { is_expected.to include(result_class.new(false, 'cannot write to storage', shard: 'default')) }
- end
- end
- end
-
- describe '#metrics' do
- context 'storage points to not existing folder' do
- let(:storages_paths) do
- {
- default: Gitlab::GitalyClient::StorageSettings.new('path' => 'tmp/this/path/doesnt/exist')
- }.with_indifferent_access
- end
-
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
- end
- end
-
- context 'storage points to directory that has both read and write rights' do
- before do
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 1))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
- end
-
- it 'cleans up files used for metrics' do
- expect { described_class.metrics }.not_to change(Dir.entries(tmp_dir), :count)
- end
- end
- end
- end
-
- context 'when timeout kills fs checks' do
- before do
- stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '1')
-
- allow(described_class).to receive(:exec_with_timeout).and_wrap_original { |m| m.call(%w(sleep 60)) }
- FileUtils.chmod_R(0755, tmp_dir)
- end
-
- describe '#readiness' do
- subject { described_class.readiness }
-
- it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: 'default')) }
- end
-
- describe '#metrics' do
- it 'provides metrics' do
- metrics = described_class.metrics
-
- expect(metrics).to all(have_attributes(labels: { shard: 'default' }))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_readable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_writable, value: 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
- expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
- end
- end
- end
-
- context 'when popen always finds required binaries' do
- before do
- allow(described_class).to receive(:exec_with_timeout).and_wrap_original do |method, *args, &block|
- begin
- method.call(*args, &block)
- rescue RuntimeError, Errno::ENOENT
- raise 'expected not to happen'
- end
- end
-
- stub_const('Gitlab::HealthChecks::FsShardsCheck::COMMAND_TIMEOUT', '10')
- end
-
- it_behaves_like 'filesystem checks'
- end
-
- context 'when popen never finds required binaries' do
- before do
- allow(Gitlab::Popen).to receive(:popen).and_raise(Errno::ENOENT)
- end
-
- it_behaves_like 'filesystem checks'
- end
-end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
index 724beefff69..4912cd48761 100644
--- a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -30,13 +30,14 @@ describe Gitlab::HealthChecks::GitalyCheck do
describe '#metrics' do
subject { described_class.metrics }
+ let(:server) { double(storage: 'default', read_writeable?: up) }
before do
- expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
+ allow(Gitaly::Server).to receive(:new).and_return(server)
end
context 'Gitaly server is up' do
- let(:gitaly_check) { double(check: { success: true }) }
+ let(:up) { true }
it 'provides metrics' do
expect(subject).to all(have_attributes(labels: { shard: 'default' }))
@@ -46,7 +47,7 @@ describe Gitlab::HealthChecks::GitalyCheck do
end
context 'Gitaly server is down' do
- let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
+ let(:up) { false }
it 'provides metrics' do
expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0))
diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
new file mode 100644
index 00000000000..6a803c48b34
--- /dev/null
+++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::GroupProjectObjectBuilder do
+ let(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ context 'labels' do
+ it 'finds the right group label' do
+ group_label = create(:group_label, 'name': 'group label', 'group': project.group)
+
+ expect(described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)).to eq(group_label)
+ end
+
+ it 'creates a new label' do
+ label = described_class.build(Label,
+ 'title' => 'group label',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(label.persisted?).to be true
+ end
+ end
+
+ context 'milestones' do
+ it 'finds the right group milestone' do
+ milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group)
+
+ expect(described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)).to eq(milestone)
+ end
+
+ it 'creates a new milestone' do
+ milestone = described_class.build(Milestone,
+ 'title' => 'group milestone',
+ 'project' => project,
+ 'group' => project.group)
+
+ expect(milestone.persisted?).to be true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
index 991e354f499..c074e61da26 100644
--- a/spec/lib/gitlab/import_export/importer_spec.rb
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -4,14 +4,14 @@ describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
- let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
+ let(:project) { create(:project, import_source: File.join(test_path, 'test_project_export.tar.gz')) }
subject(:importer) { described_class.new(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path)
- FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
+ FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path)
allow(subject).to receive(:remove_import_file)
end
diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json
index c13cf4a0507..ba2248073f5 100644
--- a/spec/lib/gitlab/import_export/project.light.json
+++ b/spec/lib/gitlab/import_export/project.light.json
@@ -7,7 +7,7 @@
"milestones": [
{
"id": 1,
- "title": "Project milestone",
+ "title": "A milestone",
"project_id": 8,
"description": "Project-level milestone",
"due_date": null,
@@ -66,8 +66,8 @@
"group_milestone_id": null,
"milestone": {
"id": 1,
- "title": "Project milestone",
- "project_id": 8,
+ "title": "A milestone",
+ "group_id": 8,
"description": "Project-level milestone",
"due_date": null,
"created_at": "2016-06-14T15:02:04.415Z",
@@ -86,7 +86,7 @@
"updated_at": "2017-08-15T18:37:40.795Z",
"label": {
"id": 6,
- "title": "Another project label",
+ "title": "Another label",
"color": "#A8D695",
"project_id": null,
"created_at": "2017-08-15T18:37:19.698Z",
diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/lib/gitlab/import_export/project.milestone-iid.json
new file mode 100644
index 00000000000..b028147b5eb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/project.milestone-iid.json
@@ -0,0 +1,80 @@
+{
+ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
+ "import_type": "gitlab_project",
+ "creator_id": 123,
+ "visibility_level": 10,
+ "archived": false,
+ "issues": [
+ {
+ "id": 1,
+ "title": "Fugiat est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 20,
+ "updated_by_id": 1,
+ "confidential": false,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 1,
+ "title": "Group-level milestone",
+ "description": "Group-level milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": 8
+ }
+ },
+ {
+ "id": 2,
+ "title": "est minima quae maxime non similique.",
+ "assignee_id": null,
+ "project_id": 8,
+ "author_id": 1,
+ "created_at": "2017-07-07T18:13:01.138Z",
+ "updated_at": "2017-08-15T18:37:40.807Z",
+ "branch_name": null,
+ "description": "Quam totam fuga numquam in eveniet.",
+ "state": "opened",
+ "iid": 21,
+ "updated_by_id": 1,
+ "confidential": false,
+ "due_date": null,
+ "moved_to_id": null,
+ "lock_version": null,
+ "time_estimate": 0,
+ "closed_at": null,
+ "last_edited_at": null,
+ "last_edited_by_id": null,
+ "group_milestone_id": null,
+ "milestone": {
+ "id": 2,
+ "title": "Another milestone",
+ "project_id": 8,
+ "description": "milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "group_id": null
+ }
+ }
+ ],
+ "snippets": [],
+ "hooks": []
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 68ddc947e02..bac5693c830 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -189,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@project.pipelines.zip([2, 2, 2, 2, 2])
.each do |(pipeline, expected_status_size)|
- expect(pipeline.statuses.size).to eq(expected_status_size)
- end
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
end
end
@@ -246,13 +246,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(project.issues.size).to eq(results.fetch(:issues, 0))
end
- it 'has issue with group label and project label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
- expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
- end
-
it 'does not set params that are excluded from import_export settings' do
expect(project.import_type).to be_nil
expect(project.creator_id).not_to eq 123
@@ -268,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it 'has group milestone' do
expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0))
end
-
- it 'has issue with group label' do
- labels = project.issues.first.labels
-
- expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0))
- end
end
context 'Light JSON' do
@@ -360,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
it_behaves_like 'restores project correctly',
issues: 2,
labels: 1,
- milestones: 1,
+ milestones: 2,
first_issue_labels: 1
it_behaves_like 'restores group correctly',
- labels: 1,
- milestones: 1,
+ labels: 0,
+ milestones: 0,
first_issue_labels: 1
end
+
+ context 'with existing group models' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ before do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json")
+ end
+
+ it 'imports labels' do
+ create(:group_label, name: 'Another label', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.labels.count).to eq(1)
+ end
+
+ it 'imports milestones' do
+ create(:milestone, name: 'A milestone', group: project.group)
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.group.milestones.count).to eq(1)
+ expect(project.milestones.count).to eq(0)
+ end
+ end
+
+ context 'with clashing milestones on IID' do
+ let!(:project) do
+ create(:project,
+ :builds_disabled,
+ :issues_disabled,
+ name: 'project',
+ path: 'project',
+ group: create(:group))
+ end
+
+ it 'preserves the project milestone IID' do
+ project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json")
+
+ expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error)
+
+ restored_project_json
+
+ expect(project.milestones.count).to eq(2)
+ expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
+ expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 013b8895f67..7ffa84f906d 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::RepoRestorer do
end
it 'restores the repo successfully' do
- expect(restorer.restore).to be true
+ expect(restorer.restore).to be_truthy
end
it 'has the webhooks' do
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 39ec2f37a83..5c398bc2063 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
+ using RSpec::Parameterized::TableSyntax
RSpec::Matchers.define :be_a_redirect do
match do |response|
@@ -117,39 +118,41 @@ describe Gitlab::Middleware::ReadOnly do
context 'whitelisted requests' do
it 'expects a POST internal request to be allowed' do
expect(Rails.application.routes).not_to receive(:recognize_path)
-
response = request.post("/api/#{API::API.version}/internal")
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
- it 'expects a POST LFS request to batch URL to be allowed' do
- expect(Rails.application.routes).to receive(:recognize_path).and_call_original
- response = request.post('/root/rouge.git/info/lfs/objects/batch')
+ it 'expects requests to sidekiq admin to be allowed' do
+ response = request.post('/admin/sidekiq')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
- end
- it 'expects a POST request to git-upload-pack URL to be allowed' do
- expect(Rails.application.routes).to receive(:recognize_path).and_call_original
- response = request.post('/root/rouge.git/git-upload-pack')
+ response = request.get('/admin/sidekiq')
expect(response).not_to be_a_redirect
expect(subject).not_to disallow_request
end
- it 'expects requests to sidekiq admin to be allowed' do
- response = request.post('/admin/sidekiq')
-
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ where(:description, :path) do
+ 'LFS request to batch' | '/root/rouge.git/info/lfs/objects/batch'
+ 'LFS request to locks verify' | '/root/rouge.git/info/lfs/locks/verify'
+ 'LFS request to locks create' | '/root/rouge.git/info/lfs/locks'
+ 'LFS request to locks unlock' | '/root/rouge.git/info/lfs/locks/1/unlock'
+ 'request to git-upload-pack' | '/root/rouge.git/git-upload-pack'
+ 'request to git-receive-pack' | '/root/rouge.git/git-receive-pack'
+ end
- response = request.get('/admin/sidekiq')
+ with_them do
+ it "expects a POST #{description} URL to be allowed" do
+ expect(Rails.application.routes).to receive(:recognize_path).and_call_original
+ response = request.post(path)
- expect(response).not_to be_a_redirect
- expect(subject).not_to disallow_request
+ expect(response).not_to be_a_redirect
+ expect(subject).not_to disallow_request
+ end
end
end
end
diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb
index 85971f2a7ef..5bd4d6c6a48 100644
--- a/spec/lib/gitlab/repository_cache_adapter_spec.rb
+++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb
@@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do
describe '#expire_method_caches' do
it 'expires the caches of the given methods' do
- expect(cache).to receive(:expire).with(:readme)
+ expect(cache).to receive(:expire).with(:rendered_readme)
expect(cache).to receive(:expire).with(:gitignore)
- repository.expire_method_caches(%i(readme gitignore))
+ repository.expire_method_caches(%i(rendered_readme gitignore))
+ end
+
+ it 'does not expire caches for non-existent methods' do
+ expect(cache).not_to receive(:expire).with(:nonexistent)
+ expect(Rails.logger).to(
+ receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository"))
+
+ repository.expire_method_caches(%i(nonexistent))
end
end
end
diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb
new file mode 100644
index 00000000000..e1a69261939
--- /dev/null
+++ b/spec/lib/gitlab/shard_health_cache_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do
+ let(:shards) { %w(foo bar) }
+
+ before do
+ described_class.update(shards)
+ end
+
+ describe '.clear' do
+ it 'leaves no shards around' do
+ described_class.clear
+
+ expect(described_class.healthy_shard_count).to eq(0)
+ end
+ end
+
+ describe '.update' do
+ it 'returns the healthy shards' do
+ expect(described_class.cached_healthy_shards).to match_array(shards)
+ end
+
+ it 'replaces the existing set' do
+ new_set = %w(test me more)
+ described_class.update(new_set)
+
+ expect(described_class.cached_healthy_shards).to match_array(new_set)
+ end
+ end
+
+ describe '.healthy_shard_count' do
+ it 'returns the healthy shard count' do
+ expect(described_class.healthy_shard_count).to eq(2)
+ end
+
+ it 'returns 0 if no shards are available' do
+ described_class.update([])
+
+ expect(described_class.healthy_shard_count).to eq(0)
+ end
+ end
+
+ describe '.healthy_shard?' do
+ it 'returns true for a healthy shard' do
+ expect(described_class.healthy_shard?('foo')).to be_truthy
+ end
+
+ it 'returns false for an unknown shard' do
+ expect(described_class.healthy_shard?('unknown')).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 5410bfbeb31..b7687d48c68 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Mattermost::Session, type: :request do
+ include ExclusiveLeaseHelpers
+
let(:user) { create(:user) }
let(:gitlab_url) { "http://gitlab.com" }
@@ -97,26 +99,20 @@ describe Mattermost::Session, type: :request do
end
end
- context 'with lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
- end
+ context 'exclusive lease' do
+ let(:lease_key) { 'mattermost:session' }
it 'tries to obtain a lease' do
- expect(subject).to receive(:lease_try_obtain)
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
# Cannot setup a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
- end
- context 'without lease' do
- before do
- allow(subject).to receive(:lease_try_obtain).and_return(nil)
- end
+ it 'returns a NoSessionError error without lease' do
+ stub_exclusive_lease_taken(lease_key)
- it 'returns a NoSessionError error' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
diff --git a/spec/migrations/cleanup_stages_position_migration_spec.rb b/spec/migrations/cleanup_stages_position_migration_spec.rb
new file mode 100644
index 00000000000..dde5a777487
--- /dev/null
+++ b/spec/migrations/cleanup_stages_position_migration_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180604123514_cleanup_stages_position_migration.rb')
+
+describe CleanupStagesPositionMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateStageIndex)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateStageIndex', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateStageIndex', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated stages present' do
+ let(:stages) { table('ci_stages') }
+ let(:builds) { table('ci_builds') }
+
+ let!(:entities) do
+ %w[build test broken].map do |name|
+ stages.create(name: name)
+ end
+ end
+
+ before do
+ stages.update_all(position: nil)
+
+ builds.create(name: 'unit', stage_id: entities.first.id, stage_idx: 1, ref: 'master')
+ builds.create(name: 'unit', stage_id: entities.second.id, stage_idx: 1, ref: 'master')
+ end
+
+ it 'migrates stages sequentially for every stage' do
+ expect(stages.all).to all(have_attributes(position: nil))
+
+ migrate!
+
+ expect(migration).to have_received(:perform)
+ .with(entities.first.id, entities.first.id)
+ expect(migration).to have_received(:perform)
+ .with(entities.second.id, entities.second.id)
+ expect(migration).not_to have_received(:perform)
+ .with(entities.third.id, entities.third.id)
+ end
+ end
+end
diff --git a/spec/migrations/enqueue_delete_diff_files_workers_spec.rb b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb
new file mode 100644
index 00000000000..686027822b8
--- /dev/null
+++ b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180619121030_enqueue_delete_diff_files_workers.rb')
+
+describe EnqueueDeleteDiffFilesWorkers, :migration, :sidekiq do
+ let(:merge_request_diffs) { table(:merge_request_diffs) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ namespaces.create!(id: 1, name: 'gitlab', path: 'gitlab')
+ projects.create!(id: 1, namespace_id: 1, name: 'gitlab', path: 'gitlab')
+
+ merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master', state: 'merged')
+
+ merge_request_diffs.create!(id: 1, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 2, merge_request_id: 1, state: 'without_files')
+ merge_request_diffs.create!(id: 3, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 4, merge_request_id: 1, state: 'collected')
+ merge_request_diffs.create!(id: 5, merge_request_id: 1, state: 'empty')
+ merge_request_diffs.create!(id: 6, merge_request_id: 1, state: 'collected')
+
+ merge_requests.update(1, latest_merge_request_diff_id: 6)
+ end
+
+ it 'correctly schedules diff file deletion workers' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ # 1st batch
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, 1)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(9.minutes, 3)
+ # 2nd batch
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(16.minutes, 4)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(17.minutes, 6)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(4)
+ end
+ end
+ end
+
+ it 'migrates the data' do
+ expect { migrate! }.to change { merge_request_diffs.where(state: 'without_files').count }
+ .from(1).to(4)
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 3e6656e0f12..02f74e2ea54 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -25,15 +25,6 @@ describe ApplicationSetting do
it { is_expected.to allow_value(https).for(:after_sign_out_path) }
it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
- describe 'disabled_oauth_sign_in_sources validations' do
- before do
- allow(Devise).to receive(:omniauth_providers).and_return([:github])
- end
-
- it { is_expected.to allow_value(['github']).for(:disabled_oauth_sign_in_sources) }
- it { is_expected.not_to allow_value(['test']).for(:disabled_oauth_sign_in_sources) }
- end
-
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -314,6 +305,33 @@ describe ApplicationSetting do
end
end
+ describe '#disabled_oauth_sign_in_sources=' do
+ before do
+ allow(Devise).to receive(:omniauth_providers).and_return([:github])
+ end
+
+ it 'removes unknown sources (as strings) from the array' do
+ subject.disabled_oauth_sign_in_sources = %w[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'removes unknown sources (as symbols) from the array' do
+ subject.disabled_oauth_sign_in_sources = %i[github test]
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to eq ['github']
+ end
+
+ it 'ignores nil' do
+ subject.disabled_oauth_sign_in_sources = nil
+
+ expect(subject).to be_valid
+ expect(subject.disabled_oauth_sign_in_sources).to be_empty
+ end
+ end
+
context 'restricted signup domains' do
it 'sets single domain' do
setting.domain_whitelist_raw = 'example.com'
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 51b9b518117..6758adc59eb 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1871,7 +1871,11 @@ describe Ci::Build do
end
context 'when yaml_variables are undefined' do
- let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id,
+ ref: project.default_branch)
+ end
before do
build.yaml_variables = nil
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 94a5fe8e5f8..97a5de47b37 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
set(:build) { create(:ci_build, :running) }
let(:chunk_index) { 0 }
let(:data_store) { :redis }
@@ -105,14 +107,12 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when the other process is appending' do
let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
- it 'raise an error' do
- begin
- uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.day).try_obtain
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
- expect { subject }.to raise_error('Failed to obtain a lock')
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
end
end
@@ -406,14 +406,12 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
context 'when the other process is persisting' do
let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
- it 'raise an error' do
- begin
- uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.day).try_obtain
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
- expect { subject }.to raise_error('Failed to obtain a lock')
- ensure
- Gitlab::ExclusiveLease.cancel(lease_key, uuid)
- end
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 090f91168ad..5157d8fc645 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -514,30 +514,21 @@ eos
end
describe '#uri_type' do
- shared_examples 'URI type' do
- it 'returns the URI type at the given path' do
- expect(commit.uri_type('files/html')).to be(:tree)
- expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
- expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
- expect(commit.uri_type('files/js/application.js')).to be(:blob)
- end
-
- it "returns nil if the path doesn't exists" do
- expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
- end
-
- it 'is nil if the path is nil or empty' do
- expect(commit.uri_type(nil)).to be_nil
- expect(commit.uri_type("")).to be_nil
- end
+ it 'returns the URI type at the given path' do
+ expect(commit.uri_type('files/html')).to be(:tree)
+ expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
+ expect(commit.uri_type('files/js/application.js')).to be(:blob)
end
- context 'when Gitaly commit_tree_entry feature is enabled' do
- it_behaves_like 'URI type'
+ it "returns nil if the path doesn't exists" do
+ expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
+ expect(commit.uri_type('../path/doesnt/exist')).to be_nil
end
- context 'when Gitaly commit_tree_entry feature is disabled', :disable_gitaly do
- it_behaves_like 'URI type'
+ it 'is nil if the path is nil or empty' do
+ expect(commit.uri_type(nil)).to be_nil
+ expect(commit.uri_type("")).to be_nil
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index f2a3df50c1a..0f156619e9e 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe ReactiveCaching, :use_clean_rails_memory_store_caching do
+ include ExclusiveLeaseHelpers
include ReactiveCachingHelpers
class CacheTest
@@ -106,8 +107,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
it 'takes and releases the lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
- expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000")
+ expect_to_obtain_exclusive_lease(cache_key, 'uuid')
+ expect_to_cancel_exclusive_lease(cache_key, 'uuid')
go!
end
@@ -153,11 +154,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
context 'when the lease is already taken' do
- before do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil)
- end
-
it 'skips the calculation' do
+ stub_exclusive_lease_taken(cache_key)
+
expect(instance).to receive(:calculate_reactive_cache).never
go!
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index 2a2ef5a304d..2f9f63ce7e0 100644
--- a/spec/models/concerns/resolvable_discussion_spec.rb
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -534,11 +534,18 @@ describe Discussion, ResolvableDiscussion do
describe "#last_resolved_note" do
let(:current_user) { create(:user) }
+ let(:time) { Time.now.utc }
before do
- first_note.resolve!(current_user)
- third_note.resolve!(current_user)
- second_note.resolve!(current_user)
+ Timecop.freeze(time - 1.second) do
+ first_note.resolve!(current_user)
+ end
+ Timecop.freeze(time) do
+ third_note.resolve!(current_user)
+ end
+ Timecop.freeze(time + 1.second) do
+ second_note.resolve!(current_user)
+ end
end
it "returns the last note that was resolved" do
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index b821a84d5e0..39c16ae60af 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -40,15 +40,25 @@ describe Sortable do
describe 'ordering by name' do
it 'ascending' do
- expect(relation).to receive(:reorder).with("lower(name) asc")
+ expect(relation).to receive(:reorder).once.and_call_original
- relation.order_by('name_asc')
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_asc').to_sql
+
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) ASC\z/
end
it 'descending' do
- expect(relation).to receive(:reorder).with("lower(name) desc")
+ expect(relation).to receive(:reorder).once.and_call_original
+
+ table = Regexp.escape(ActiveRecord::Base.connection.quote_table_name(:namespaces))
+ column = Regexp.escape(ActiveRecord::Base.connection.quote_column_name(:name))
+
+ sql = relation.order_by('name_desc').to_sql
- relation.order_by('name_desc')
+ expect(sql).to match /.+ORDER BY LOWER\(#{table}.#{column}\) DESC\z/
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index b4249d72fc8..48c01fc4d4e 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -47,6 +47,45 @@ describe MergeRequestDiff do
end
describe '#diffs' do
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let!(:diff) { merge_request.merge_request_diff.reload }
+
+ context 'when it was not cleaned by the system' do
+ it 'returns persisted diffs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.diffs
+ end
+ end
+
+ context 'when diff was cleaned by the system' do
+ before do
+ diff.clean!
+ end
+
+ it 'returns diffs from repository if can compare with current diff refs' do
+ expect(diff).not_to receive(:load_diffs)
+
+ expect(Compare)
+ .to receive(:new)
+ .with(instance_of(Gitlab::Git::Compare), merge_request.target_project,
+ base_sha: diff.base_commit_sha, straight: false)
+ .and_call_original
+
+ diff.diffs
+ end
+
+ it 'returns persisted diffs if cannot compare with diff refs' do
+ expect(diff).to receive(:load_diffs)
+
+ diff.update!(head_commit_sha: 'invalid-sha')
+
+ diff.diffs
+ end
+ end
+ end
+
+ describe '#raw_diffs' do
context 'when the :ignore_whitespace_change option is set' do
it 'creates a new compare object instead of loading from the DB' do
expect(diff_with_commits).not_to receive(:load_diffs)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 7ae70c3afb4..8c6b411ec9a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1630,28 +1630,17 @@ describe MergeRequest do
end
describe "#reload_diff" do
- let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
- let(:commit) { subject.project.commit(sample_commit.id) }
+ it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
+ user = create(:user)
+ service = instance_double(MergeRequests::ReloadDiffsService, execute: nil)
- it "does not change existing merge request diff" do
- expect(subject.merge_request_diff).not_to receive(:save_git_content)
- subject.reload_diff
- end
+ expect(MergeRequests::ReloadDiffsService)
+ .to receive(:new).with(subject, user)
+ .and_return(service)
- it "creates new merge request diff" do
- expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
- end
+ subject.reload_diff(user)
- it "executes diff cache service" do
- expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject, an_instance_of(MergeRequestDiff))
-
- subject.reload_diff
- end
-
- it "calls update_diff_discussion_positions" do
- expect(subject).to receive(:update_diff_discussion_positions)
-
- subject.reload_diff
+ expect(service).to have_received(:execute)
end
context 'when using the after_update hook to update' do
@@ -2145,8 +2134,7 @@ describe MergeRequest do
describe 'transition to cannot_be_merged' do
let(:notification_service) { double(:notification_service) }
let(:todo_service) { double(:todo_service) }
-
- subject { create(:merge_request, merge_status: :unchecked) }
+ subject { create(:merge_request, state, merge_status: :unchecked) }
before do
allow(NotificationService).to receive(:new).and_return(notification_service)
@@ -2155,33 +2143,68 @@ describe MergeRequest do
allow(subject.project.repository).to receive(:can_be_merged?).and_return(false)
end
- it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
- expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
- expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
+ [:opened, :locked].each do |state|
+ context state do
+ let(:state) { state }
+
+ it 'notifies conflict, but does not notify again if rechecking still results in cannot_be_merged' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).once
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).once
+
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'notifies conflict, whenever newly unmergeable' do
+ expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
+ expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
- subject.mark_as_unmergeable
- subject.mark_as_unchecked
- subject.mark_as_unmergeable
+ subject.mark_as_unmergeable
+ subject.mark_as_unchecked
+ subject.mark_as_mergeable
+ subject.mark_as_unchecked
+ subject.mark_as_unmergeable
+ end
+
+ it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
+
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+
+ subject.mark_as_unmergeable
+ end
+ end
end
- it 'notifies conflict, whenever newly unmergeable' do
- expect(notification_service).to receive(:merge_request_unmergeable).with(subject).twice
- expect(todo_service).to receive(:merge_request_became_unmergeable).with(subject).twice
+ [:closed, :merged].each do |state|
+ let(:state) { state }
+
+ context state do
+ it 'does not notify' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
- subject.mark_as_unmergeable
- subject.mark_as_unchecked
- subject.mark_as_mergeable
- subject.mark_as_unchecked
- subject.mark_as_unmergeable
+ subject.mark_as_unmergeable
+ end
+ end
end
- it 'does not notify whenever merge request is newly unmergeable due to other reasons' do
- allow(subject.project.repository).to receive(:can_be_merged?).and_return(true)
+ context 'source branch is missing' do
+ subject { create(:merge_request, :invalid, :opened, merge_status: :unchecked, target_branch: 'master') }
+
+ before do
+ allow(subject.project.repository).to receive(:can_be_merged?).and_call_original
+ end
- expect(notification_service).not_to receive(:merge_request_unmergeable)
- expect(todo_service).not_to receive(:merge_request_became_unmergeable)
+ it 'does not raise error' do
+ expect(notification_service).not_to receive(:merge_request_unmergeable)
+ expect(todo_service).not_to receive(:merge_request_became_unmergeable)
- subject.mark_as_unmergeable
+ expect { subject.mark_as_unmergeable }.not_to raise_error
+ expect(subject.cannot_be_merged?).to eq(true)
+ end
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 18b01c3e6b7..70f1a1c8b38 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -655,6 +655,19 @@ describe Namespace do
end
end
+ describe '#root_ancestor' do
+ it 'returns the top most ancestor', :nested_groups do
+ root_group = create(:group)
+ nested_group = create(:group, parent: root_group)
+ deep_nested_group = create(:group, parent: nested_group)
+ very_deep_nested_group = create(:group, parent: deep_nested_group)
+
+ expect(nested_group.root_ancestor).to eq(root_group)
+ expect(deep_nested_group.root_ancestor).to eq(root_group)
+ expect(very_deep_nested_group.root_ancestor).to eq(root_group)
+ end
+ end
+
describe '#remove_exports' do
let(:legacy_project) { create(:project, :with_export, :legacy_storage, namespace: namespace) }
let(:hashed_project) { create(:project, :with_export, namespace: namespace) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index a2f8fac2f38..abdc65336ca 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -571,13 +571,13 @@ describe Project do
last_activity_at: timestamp,
last_repository_updated_at: timestamp - 1.hour)
- expect(project.last_activity_date).to eq(timestamp)
+ expect(project.last_activity_date).to be_like_time(timestamp)
project.update_attributes(updated_at: timestamp,
last_activity_at: timestamp - 1.hour,
last_repository_updated_at: nil)
- expect(project.last_activity_date).to eq(timestamp)
+ expect(project.last_activity_date).to be_like_time(timestamp)
end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index d817a8376f4..cfa78c4472c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -46,7 +46,7 @@ describe Repository do
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
@@ -192,7 +192,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
@@ -226,7 +226,7 @@ describe Repository do
is_expected.to eq('c1acaa5')
end
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
@@ -391,7 +391,7 @@ describe Repository do
it_behaves_like 'finding commits by message'
end
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
@@ -479,6 +479,14 @@ describe Repository do
end
end
+ context 'when ref is not specified' do
+ it 'is using a root ref' do
+ expect(repository).to receive(:find_commit).with('master')
+
+ repository.commit
+ end
+ end
+
context 'when ref is not valid' do
context 'when preceding tree element exists' do
it 'returns nil' do
@@ -664,7 +672,7 @@ describe Repository do
end
end
- shared_examples "search_files_by_content" do
+ describe "search_files_by_content" do
let(:results) { repository.search_files_by_content('feature', 'master') }
subject { results }
@@ -695,7 +703,7 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
@@ -711,7 +719,7 @@ describe Repository do
end
end
- shared_examples "search_files_by_name" do
+ describe "search_files_by_name" do
let(:results) { repository.search_files_by_name('files', 'master') }
it 'returns result' do
@@ -744,23 +752,13 @@ describe Repository do
expect(results).to match_array([])
end
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
end
end
- describe 'with gitaly enabled' do
- it_behaves_like 'search_files_by_content'
- it_behaves_like 'search_files_by_name'
- end
-
- describe 'with gitaly disabled', :disable_gitaly do
- it_behaves_like 'search_files_by_content'
- it_behaves_like 'search_files_by_name'
- end
-
describe '#async_remove_remote' do
before do
masterrev = repository.find_branch('master').dereferenced_target
@@ -796,7 +794,7 @@ describe Repository do
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }
- describe 'when storage is broken', :broken_storage do
+ describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
@@ -1699,19 +1697,29 @@ describe Repository do
end
describe '#after_change_head' do
- it 'flushes the readme cache' do
+ it 'flushes the method caches' do
expect(repository).to receive(:expire_method_caches).with([
- :readme,
+ :size,
+ :commit_count,
+ :rendered_readme,
+ :contribution_guide,
:changelog,
- :license,
- :contributing,
+ :license_blob,
+ :license_key,
:gitignore,
- :koding,
- :gitlab_ci,
+ :koding_yml,
+ :gitlab_ci_yml,
+ :branch_names,
+ :tag_names,
+ :branch_count,
+ :tag_count,
:avatar,
- :issue_template,
- :merge_request_template,
- :xcode_config
+ :exists?,
+ :root_ref,
+ :has_visible_content?,
+ :issue_template_names,
+ :merge_request_template_names,
+ :xcode_project?
])
repository.after_change_head
@@ -2294,6 +2302,28 @@ describe Repository do
end
end
+ describe '#local_branches' do
+ it 'returns the local branches' do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ repository.add_branch(user, 'local_branch', masterrev.id)
+
+ expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
+ expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ end
+ end
+
+ describe '#remote_branches' do
+ it 'returns the remote branches' do
+ masterrev = repository.find_branch('master').dereferenced_target
+ create_remote_branch('joe', 'remote_branch', masterrev)
+ repository.add_branch(user, 'local_branch', masterrev.id)
+
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'local_branch' }).to eq(false)
+ expect(repository.remote_branches('joe').any? { |branch| branch.name == 'remote_branch' }).to eq(true)
+ end
+ end
+
describe '#commit_count' do
context 'with a non-existing repository' do
it 'returns 0' do
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index 92b614b087e..7710f19ce4e 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe API::Boards do
set(:user) { create(:user) }
- set(:user2) { create(:user) }
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 64f51d9843d..9bb6ed62393 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -155,6 +155,12 @@ describe API::Branches do
end
it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
end
context 'when unauthenticated', 'and project is private' do
@@ -169,6 +175,12 @@ describe API::Branches do
it_behaves_like 'repository branch'
+ it 'returns that the current user can push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(true)
+ end
+
context 'when branch contains a dot' do
let(:branch_name) { branch_with_dot.name }
@@ -202,6 +214,23 @@ describe API::Branches do
end
end
+ context 'when authenticated', 'as a developer and branch is protected' do
+ let(:current_user) { create(:user) }
+ let!(:protected_branch) { create(:protected_branch, project: project, name: branch_name) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it_behaves_like 'repository branch'
+
+ it 'returns that the current user cannot push' do
+ get api(route, current_user)
+
+ expect(json_response['can_push']).to eq(false)
+ end
+ end
+
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
let(:request) { get api(route, guest) }
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index d8fdfd6dee1..4bc5d3ee899 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -21,6 +21,89 @@ describe API::Files do
"/projects/#{project.id}/repository/files/#{file_path}"
end
+ describe "HEAD /projects/:id/repository/files/:file_path" do
+ shared_examples_for 'repository files' do
+ it 'returns file attributes in headers' do
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers['X-Gitlab-File-Path']).to eq(CGI.unescape(file_path))
+ expect(response.headers['X-Gitlab-File-Name']).to eq('popen.rb')
+ expect(response.headers['X-Gitlab-Last-Commit-Id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
+ end
+
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers['X-Gitlab-File-Name']).to eq('commit.js.coffee')
+ expect(response.headers['X-Gitlab-Content-Sha256']).to eq('08785f04375b47f81f46e68cc125d5ef368aa20576ddb53f91f4d83f1d04b929')
+ end
+
+ context 'when mandatory params are not given' do
+ it "responds with a 400 status" do
+ head api(route("any%2Ffile"), current_user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+
+ context 'when file_path does not exist' do
+ it "responds with a 404 status" do
+ params[:ref] = 'master'
+
+ head api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when file_path does not exist' do
+ include_context 'disabled repository'
+
+ it "responds with a 403 status" do
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository files' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it "responds with a 404 status" do
+ current_user = nil
+
+ head api(route(file_path), current_user), params
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository files' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { head api(route(file_path), guest), params }
+ end
+ end
+ end
+
describe "GET /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
it 'returns file attributes as json' do
@@ -30,6 +113,7 @@ describe API::Files do
expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
+ expect(json_response['content_sha256']).to eq('c440cd09bae50c4632cc58638ad33c6aa375b6109d811e76a9cc3a613c1e8887')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
@@ -51,6 +135,7 @@ describe API::Files do
expect(response).to have_gitlab_http_status(200)
expect(json_response['file_name']).to eq('commit.js.coffee')
+ expect(json_response['content_sha256']).to eq('08785f04375b47f81f46e68cc125d5ef368aa20576ddb53f91f4d83f1d04b929')
expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
new file mode 100644
index 00000000000..ad57c43bc87
--- /dev/null
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe 'getting merge request information nested in a project' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository, :public) }
+ let(:current_user) { create(:user) }
+ let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('mergeRequest', iid: merge_request.iid)
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it 'contains merge request information' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data).not_to be_nil
+ end
+
+ # This is a field coming from the `MergeRequestPresenter`
+ it 'includes a web_url' do
+ post_graphql(query, current_user: current_user)
+
+ expect(merge_request_graphql_data['webUrl']).to be_present
+ end
+
+ context 'permissions on the merge request' do
+ it 'includes the permissions for the current user on a public project' do
+ expected_permissions = {
+ 'readMergeRequest' => true,
+ 'adminMergeRequest' => false,
+ 'createNote' => true,
+ 'pushToSourceBranch' => false,
+ 'removeSourceBranch' => false,
+ 'cherryPickOnCurrentMergeRequest' => false,
+ 'revertOnCurrentMergeRequest' => false,
+ 'updateMergeRequest' => false
+ }
+ post_graphql(query, current_user: current_user)
+
+ permission_data = merge_request_graphql_data['userPermissions']
+
+ expect(permission_data).to be_present
+ expect(permission_data).to eq(expected_permissions)
+ end
+ end
+
+ context 'when the user does not have access to the merge request' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it 'returns nil' do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ post_graphql(query)
+
+ expect(merge_request_graphql_data).to be_nil
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb
index 796ffc9d569..a2b3dc5d121 100644
--- a/spec/requests/api/graphql/project_query_spec.rb
+++ b/spec/requests/api/graphql/project_query_spec.rb
@@ -26,50 +26,6 @@ describe 'getting project information' do
post_graphql(query, current_user: current_user)
end
end
-
- context 'when requesting a nested merge request' do
- let(:merge_request) { create(:merge_request, source_project: project) }
- let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] }
-
- let(:query) do
- graphql_query_for(
- 'project',
- { 'fullPath' => project.full_path },
- query_graphql_field('mergeRequest', iid: merge_request.iid)
- )
- end
-
- it_behaves_like 'a working graphql query' do
- before do
- post_graphql(query, current_user: current_user)
- end
- end
-
- it 'contains merge request information' do
- post_graphql(query, current_user: current_user)
-
- expect(merge_request_graphql_data).not_to be_nil
- end
-
- # This is a field coming from the `MergeRequestPresenter`
- it 'includes a web_url' do
- post_graphql(query, current_user: current_user)
-
- expect(merge_request_graphql_data['webUrl']).to be_present
- end
-
- context 'when the user does not have access to the merge request' do
- let(:project) { create(:project, :public, :repository) }
-
- it 'returns nil' do
- project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
-
- post_graphql(query)
-
- expect(merge_request_graphql_data).to be_nil
- end
- end
- end
end
context 'when the user does not have access to the project' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index a15d60aafe0..95eff029f98 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1679,7 +1679,7 @@ describe API::Issues do
let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
context 'when unauthenticated' do
- it "returns unautorized" do
+ it "returns unauthorized" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
expect(response).to have_gitlab_http_status(401)
@@ -1695,7 +1695,7 @@ describe API::Issues do
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d4ebfc3f782..eba39bb6ccc 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -14,6 +14,7 @@ describe API::MergeRequests do
let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let!(:label) do
@@ -85,7 +86,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), scope: :all
- expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request)
+ expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked)
expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
@@ -158,7 +159,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -166,7 +167,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -219,6 +220,14 @@ describe API::MergeRequests do
expect_response_ordered_exactly(merge_request)
end
end
+
+ context 'state param' do
+ it 'returns merge requests with the given state' do
+ get api('/merge_requests', user), state: 'locked'
+
+ expect_response_contain_exactly(merge_request_locked)
+ end
+ end
end
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4a2289ca137..a3b5e8c6223 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -25,7 +25,7 @@ describe API::ProjectSnippets do
expect(response).to have_gitlab_http_status(404)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 99103039f77..abf9ad738bd 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1990,6 +1990,38 @@ describe API::Projects do
end
end
+ describe 'PUT /projects/:id/transfer' do
+ context 'when authenticated as owner' do
+ let(:group) { create :group }
+
+ it 'transfers the project to the new namespace' do
+ group.add_owner(user)
+
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'fails when transferring to a non owned namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: group.id
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails when transferring to an unknown namespace' do
+ put api("/projects/#{project.id}/transfer", user), namespace: 'unknown'
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'fails on missing namespace' do
+ put api("/projects/#{project.id}/transfer", user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
it_behaves_like 'custom attributes endpoints', 'projects' do
let(:attributable) { project }
let(:other_attributable) { project2 }
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index cd135dfc32a..28f8564ae92 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -288,6 +288,9 @@ describe API::Repositories do
shared_examples_for 'repository compare' do
it "compares branches" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: false
+ }).and_call_original
get api(route, current_user), from: 'master', to: 'feature'
expect(response).to have_gitlab_http_status(200)
@@ -295,6 +298,28 @@ describe API::Repositories do
expect(json_response['diffs']).to be_present
end
+ it "compares branches with explicit merge-base mode" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: false
+ }).and_call_original
+ get api(route, current_user), from: 'master', to: 'feature', straight: false
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+
+ it "compares branches with explicit straight mode" do
+ expect(::Gitlab::Git::Compare).to receive(:new).with(anything, anything, anything, {
+ straight: true
+ }).and_call_original
+ get api(route, current_user), from: 'master', to: 'feature', straight: true
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+
it "compares tags" do
get api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index c5456977b60..6da769cb3ed 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -314,7 +314,7 @@ describe API::Snippets do
expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
end
- it "returns unautorized for non-admin users" do
+ it "returns unauthorized for non-admin users" do
get api("/snippets/#{snippet.id}/user_agent_detail", user)
expect(response).to have_gitlab_http_status(403)
diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb
new file mode 100644
index 00000000000..000c3a2b868
--- /dev/null
+++ b/spec/requests/oauth_tokens_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe 'OAuth Tokens requests' do
+ let(:user) { create :user }
+ let(:application) { create :oauth_application, scopes: 'api' }
+
+ def request_access_token(user)
+ post '/oauth/token',
+ grant_type: 'authorization_code',
+ code: generate_access_grant(user).token,
+ redirect_uri: application.redirect_uri,
+ client_id: application.uid,
+ client_secret: application.secret
+ end
+
+ def generate_access_grant(user)
+ create :oauth_access_grant, application: application, resource_owner_id: user.id
+ end
+
+ context 'when there is already a token for the application' do
+ let!(:existing_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
+
+ context 'and the request is done by the resource owner' do
+ it 'reuses and returns the stored token' do
+ expect do
+ request_access_token(user)
+ end.not_to change { Doorkeeper::AccessToken.count }
+
+ expect(json_response['access_token']).to eq existing_token.token
+ end
+ end
+
+ context 'and the request is done by a different user' do
+ let(:other_user) { create :user }
+
+ it 'generates and returns a different token for a different owner' do
+ expect do
+ request_access_token(other_user)
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
+
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+ end
+
+ context 'when there is no token stored for the application' do
+ it 'generates and returns a new token' do
+ expect do
+ request_access_token(user)
+ end.to change { Doorkeeper::AccessToken.count }.by(1)
+
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index bcb8d6c2bfc..b14d4b8fb6e 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -1,11 +1,49 @@
require 'spec_helper'
describe 'OpenID Connect requests' do
- let(:user) { create :user }
+ let(:user) do
+ create(
+ :user,
+ name: 'Alice',
+ username: 'alice',
+ email: 'private@example.com',
+ emails: [public_email],
+ public_email: public_email.email,
+ website_url: 'https://example.com',
+ avatar: fixture_file_upload('spec/fixtures/dk.png')
+ )
+ end
+
+ let(:public_email) { build :email, email: 'public@example.com' }
+
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
- def request_access_token
+ let(:hashed_subject) do
+ Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
+ end
+
+ let(:id_token_claims) do
+ {
+ 'sub' => user.id.to_s,
+ 'sub_legacy' => hashed_subject
+ }
+ end
+
+ let(:user_info_claims) do
+ {
+ 'name' => 'Alice',
+ 'nickname' => 'alice',
+ 'email' => 'public@example.com',
+ 'email_verified' => true,
+ 'website' => 'https://example.com',
+ 'profile' => 'http://localhost/alice',
+ 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
+ 'groups' => kind_of(Array)
+ }
+ end
+
+ def request_access_token!
login_as user
post '/oauth/token',
@@ -16,26 +54,22 @@ describe 'OpenID Connect requests' do
client_secret: application.secret
end
- def request_user_info
+ def request_user_info!
get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
end
- def hashed_subject
- Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
- end
-
context 'Application without OpenID scope' do
let(:application) { create :oauth_application, scopes: 'api' }
it 'token response does not include an ID token' do
- request_access_token
+ request_access_token!
expect(json_response).to include 'access_token'
expect(json_response).not_to include 'id_token'
end
it 'userinfo response is unauthorized' do
- request_user_info
+ request_user_info!
expect(response).to have_gitlab_http_status 403
expect(response.body).to be_blank
@@ -46,28 +80,12 @@ describe 'OpenID Connect requests' do
let(:application) { create :oauth_application, scopes: 'openid' }
it 'token response includes an ID token' do
- request_access_token
+ request_access_token!
expect(json_response).to include 'id_token'
end
context 'UserInfo payload' do
- let(:user) do
- create(
- :user,
- name: 'Alice',
- username: 'alice',
- emails: [private_email, public_email],
- email: private_email.email,
- public_email: public_email.email,
- website_url: 'https://example.com',
- avatar: fixture_file_upload('spec/fixtures/dk.png')
- )
- end
-
- let!(:public_email) { build :email, email: 'public@example.com' }
- let!(:private_email) { build :email, email: 'private@example.com' }
-
let!(:group1) { create :group }
let!(:group2) { create :group }
let!(:group3) { create :group, parent: group2 }
@@ -76,41 +94,35 @@ describe 'OpenID Connect requests' do
before do
group1.add_user(user, GroupMember::OWNER)
group3.add_user(user, Gitlab::Access::DEVELOPER)
+
+ request_user_info!
end
it 'includes all user information and group memberships' do
- request_user_info
-
- expect(json_response).to match(a_hash_including({
- 'sub' => hashed_subject,
- 'name' => 'Alice',
- 'nickname' => 'alice',
- 'email' => 'public@example.com',
- 'email_verified' => true,
- 'website' => 'https://example.com',
- 'profile' => 'http://localhost/alice',
- 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
- 'groups' => anything
- }))
+ expect(json_response).to match(id_token_claims.merge(user_info_claims))
expected_groups = [group1.full_path, group3.full_path]
expected_groups << group4.full_path if Group.supports_nested_groups?
expect(json_response['groups']).to match_array(expected_groups)
end
+
+ it 'does not include any unknown claims' do
+ expect(json_response.keys).to eq %w[sub sub_legacy] + user_info_claims.keys
+ end
end
context 'ID token payload' do
before do
- request_access_token
+ request_access_token!
@payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
end
- it 'includes the Gitlab root URL' do
- expect(@payload['iss']).to eq Gitlab.config.gitlab.url
+ it 'includes the subject claims' do
+ expect(@payload).to match(a_hash_including(id_token_claims))
end
- it 'includes the hashed user ID' do
- expect(@payload['sub']).to eq hashed_subject
+ it 'includes the Gitlab root URL' do
+ expect(@payload['iss']).to eq Gitlab.config.gitlab.url
end
it 'includes the time of the last authentication', :clean_gitlab_redis_shared_state do
@@ -118,7 +130,7 @@ describe 'OpenID Connect requests' do
end
it 'does not include any unknown properties' do
- expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
+ expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time sub_legacy]
end
end
@@ -134,10 +146,10 @@ describe 'OpenID Connect requests' do
context 'when user is blocked' do
it 'returns authentication error' do
access_grant
- user.block
+ user.block!
expect do
- request_access_token
+ request_access_token!
end.to raise_error UncaughtThrowError
end
end
@@ -145,10 +157,10 @@ describe 'OpenID Connect requests' do
context 'when user is ldap_blocked' do
it 'returns authentication error' do
access_grant
- user.ldap_block
+ user.ldap_block!
expect do
- request_access_token
+ request_access_token!
end.to raise_error UncaughtThrowError
end
end
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
index bf038595a4d..eb0bdb61ee3 100644
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
@@ -1,11 +1,13 @@
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
+ include ExclusiveLeaseHelpers
+
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
let(:ingress) { [{ ip: '111.222.111.222' }] }
- let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
+ let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
let(:kube_service) do
::Kubeclient::Resource.new(
@@ -22,11 +24,8 @@ describe Clusters::Applications::CheckIngressIpAddressService do
subject { service.execute }
before do
+ stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i)
allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
- allow(Gitlab::ExclusiveLease)
- .to receive(:new)
- .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
- .and_return(exclusive_lease)
end
describe '#execute' do
@@ -47,13 +46,9 @@ describe Clusters::Applications::CheckIngressIpAddressService do
end
context 'when the exclusive lease cannot be obtained' do
- before do
- allow(exclusive_lease)
- .to receive(:try_obtain)
- .and_return(false)
- end
-
it 'does not call kubeclient' do
+ stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i)
+
subject
expect(kubeclient).not_to have_received(:get_service)
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index a9aee9e100f..609eef76d2c 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -5,8 +5,11 @@ describe Issues::MoveService do
let(:author) { create(:user) }
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:group) { create(:group, :private) }
+ let(:sub_group_1) { create(:group, :private, parent: group) }
+ let(:sub_group_2) { create(:group, :private, parent: group) }
+ let(:old_project) { create(:project, namespace: sub_group_1) }
+ let(:new_project) { create(:project, namespace: sub_group_2) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
@@ -14,7 +17,7 @@ describe Issues::MoveService do
project: old_project, author: author, milestone: milestone1)
end
- let(:move_service) do
+ subject(:move_service) do
described_class.new(old_project, user)
end
@@ -102,6 +105,23 @@ describe Issues::MoveService do
end
end
+ context 'issue with group labels', :nested_groups do
+ it 'assigns group labels to new issue' do
+ label = create(:group_label, group: group)
+ label_issue = create(:labeled_issue, description: description, project: old_project,
+ milestone: milestone1, labels: [label])
+ old_project.add_reporter(user)
+ new_project.add_reporter(user)
+
+ new_issue = move_service.execute(label_issue, new_project)
+
+ expect(new_issue).to have_attributes(
+ project: new_project,
+ labels: include(label)
+ )
+ end
+ end
+
context 'generic issue' do
include_context 'issue move executed'
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
index bb0fb6acf39..8e553c2f1fa 100644
--- a/spec/services/keys/last_used_service_spec.rb
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -8,7 +8,7 @@ describe Keys::LastUsedService do
Timecop.freeze(time) { described_class.new(key).execute }
- expect(key.last_used_at).to eq(time)
+ expect(key.reload.last_used_at).to be_like_time(time)
end
it 'does not update the key when it has been used recently' do
@@ -17,7 +17,7 @@ describe Keys::LastUsedService do
described_class.new(key).execute
- expect(key.last_used_at).to eq(time)
+ expect(key.last_used_at).to be_like_time(time)
end
it 'does not update the updated_at field' do
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
new file mode 100644
index 00000000000..1c632847940
--- /dev/null
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state do
+ let(:merge_request) { create(:merge_request) }
+
+ let!(:subject) { described_class.new(merge_request) }
+
+ describe '#execute' do
+ before do
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ 3.times { merge_request.create_merge_request_diff }
+ end
+
+ it 'schedules non-latest merge request diffs removal' do
+ diffs = merge_request.merge_request_diffs
+
+ expect(diffs.count).to eq(4)
+
+ Timecop.freeze do
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(5.minutes, [[diffs.first.id], [diffs.second.id]])
+ expect(DeleteDiffFilesWorker)
+ .to receive(:bulk_perform_in)
+ .with(10.minutes, [[diffs.third.id]])
+
+ subject.execute
+ end
+ end
+
+ it 'schedules no removal if it is already cleaned' do
+ merge_request.merge_request_diffs.each(&:clean!)
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if it is empty' do
+ merge_request.merge_request_diffs.each { |diff| diff.update!(state: :empty) }
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+
+ it 'schedules no removal if there is no non-latest diffs' do
+ merge_request
+ .merge_request_diffs
+ .where.not(id: merge_request.latest_merge_request_diff_id)
+ .destroy_all
+
+ expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
+
+ subject.execute
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
deleted file mode 100644
index 57b6165cfb0..00000000000
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequests::MergeRequestDiffCacheService, :use_clean_rails_memory_store_caching do
- let(:subject) { described_class.new }
- let(:merge_request) { create(:merge_request) }
-
- describe '#execute' do
- before do
- allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
- allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
- end
-
- it 'retrieves the diff files to cache the highlighted result' do
- new_diff = merge_request.merge_request_diff
- cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
-
- it 'clears the cache for older diffs on the merge request' do
- old_diff = merge_request.merge_request_diff
- old_cache_key = old_diff.diffs.cache_key
-
- subject.execute(merge_request, old_diff)
-
- new_diff = merge_request.create_merge_request_diff
- new_cache_key = new_diff.diffs.cache_key
-
- expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
- expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
- expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
-
- subject.execute(merge_request, new_diff)
- end
- end
-end
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 70957431942..46e4e3559dc 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -35,5 +35,30 @@ describe MergeRequests::PostMergeService do
described_class.new(project, user, {}).execute(merge_request)
end
+
+ it 'deletes non-latest diffs' do
+ diff_removal_service = instance_double(MergeRequests::DeleteNonLatestDiffsService, execute: nil)
+
+ expect(MergeRequests::DeleteNonLatestDiffsService)
+ .to receive(:new).with(merge_request)
+ .and_return(diff_removal_service)
+
+ described_class.new(project, user, {}).execute(merge_request)
+
+ expect(diff_removal_service).to have_received(:execute)
+ end
+
+ it 'marks MR as merged regardless of errors when closing issues' do
+ merge_request.update(target_branch: 'foo')
+ allow(project).to receive(:default_branch).and_return('foo')
+
+ issue = create(:issue, project: project)
+ allow(merge_request).to receive(:closes_issues).and_return([issue])
+ allow_any_instance_of(Issues::CloseService).to receive(:execute).with(issue, commit: merge_request).and_raise
+
+ expect { described_class.new(project, user, {}).execute(merge_request) }.to raise_error
+
+ expect(merge_request.reload).to be_merged
+ end
end
end
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
new file mode 100644
index 00000000000..a0a27d247fc
--- /dev/null
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching do
+ let(:current_user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:subject) { described_class.new(merge_request, current_user) }
+
+ describe '#execute' do
+ it 'creates new merge request diff' do
+ expect { subject.execute }.to change { merge_request.merge_request_diffs.count }.by(1)
+ end
+
+ it 'calls update_diff_discussion_positions with correct params' do
+ old_diff_refs = merge_request.diff_refs
+ new_diff = merge_request.create_merge_request_diff
+ new_diff_refs = merge_request.diff_refs
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(merge_request).to receive(:update_diff_discussion_positions)
+ .with(old_diff_refs: old_diff_refs,
+ new_diff_refs: new_diff_refs,
+ current_user: current_user)
+
+ subject.execute
+ end
+
+ it 'does not change existing merge request diff' do
+ expect(merge_request.merge_request_diff).not_to receive(:save_git_content)
+
+ subject.execute
+ end
+
+ context 'cache clearing' do
+ before do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
+ end
+
+ it 'retrieves the diff files to cache the highlighted result' do
+ new_diff = merge_request.create_merge_request_diff
+ cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
+
+ subject.execute
+ end
+
+ it 'clears the cache for older diffs on the merge request' do
+ old_diff = merge_request.merge_request_diff
+ old_cache_key = old_diff.diffs_collection.cache_key
+ new_diff = merge_request.create_merge_request_diff
+ new_cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
+ expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
+ subject.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/batch_open_issues_count_service_spec.rb b/spec/services/projects/batch_open_issues_count_service_spec.rb
new file mode 100644
index 00000000000..599aaf62080
--- /dev/null
+++ b/spec/services/projects/batch_open_issues_count_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Projects::BatchOpenIssuesCountService do
+ let!(:project_1) { create(:project) }
+ let!(:project_2) { create(:project) }
+
+ let(:subject) { described_class.new([project_1, project_2]) }
+
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ before do
+ create(:issue, project: project_1)
+ create(:issue, project: project_1, confidential: true)
+
+ create(:issue, project: project_2)
+ create(:issue, project: project_2, confidential: true)
+ end
+
+ context 'when cache is clean' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ # It does not update total issues cache
+ expect(Rails.cache.read(get_cache_key(subject, project_1))).to eq(nil)
+ expect(Rails.cache.read(get_cache_key(subject, project_2))).to eq(nil)
+
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ expect(Rails.cache.read(get_cache_key(subject, project_1, true))).to eq(1)
+ end
+ end
+
+ context 'when issues count is already cached' do
+ before do
+ create(:issue, project: project_2)
+ subject.refresh_cache
+ end
+
+ it 'does update cache again' do
+ expect(Rails.cache).not_to receive(:write)
+
+ subject.refresh_cache
+ end
+ end
+ end
+
+ def get_cache_key(subject, project, public_key = false)
+ service = subject.count_service.new(project)
+
+ if public_key
+ service.cache_key(service.class::PUBLIC_COUNT_KEY)
+ else
+ service.cache_key(service.class::TOTAL_COUNT_KEY)
+ end
+ end
+end
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index 06b470849b3..562c14a8df8 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -50,5 +50,40 @@ describe Projects::OpenIssuesCountService do
end
end
end
+
+ context '#refresh_cache', :use_clean_rails_memory_store_caching do
+ let(:subject) { described_class.new(project) }
+
+ before do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+ end
+
+ context 'when cache is empty' do
+ it 'refreshes cache keys correctly' do
+ subject.refresh_cache
+
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(2)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(3)
+ end
+ end
+
+ context 'when cache is outdated' do
+ before do
+ subject.refresh_cache
+ end
+
+ it 'refreshes cache keys correctly' do
+ create(:issue, :opened, project: project)
+ create(:issue, :opened, confidential: true, project: project)
+
+ subject.refresh_cache
+
+ expect(Rails.cache.read(subject.cache_key(described_class::PUBLIC_COUNT_KEY))).to eq(3)
+ expect(Rails.cache.read(subject.cache_key(described_class::TOTAL_COUNT_KEY))).to eq(5)
+ end
+ end
+ end
end
end
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index 723cb374c37..5c2e79ff9af 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
+ let(:owner) { project.owner }
let(:remote_project) { create(:forked_project_with_submodules) }
let(:repository) { project.repository }
let(:raw_repository) { repository.raw }
@@ -9,13 +10,11 @@ describe Projects::UpdateRemoteMirrorService do
subject { described_class.new(project, project.creator) }
- describe "#execute", :skip_gitaly_mock do
+ describe "#execute" do
before do
- create_branch(repository, 'existing-branch')
- allow(raw_repository).to receive(:remote_tags) do
- generate_tags(repository, 'v1.0.0', 'v1.1.0')
- end
- allow(raw_repository).to receive(:push_remote_branches).and_return(true)
+ repository.add_branch(owner, 'existing-branch', 'master')
+
+ allow(remote_mirror).to receive(:update_repository).and_return(true)
end
it "fetches the remote repository" do
@@ -34,307 +33,57 @@ describe Projects::UpdateRemoteMirrorService do
expect(result[:status]).to eq(:success)
end
- describe 'Syncing branches' do
+ context 'when syncing all branches' do
it "push all the branches the first time" do
allow(repository).to receive(:fetch_remote)
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
-
- subject.execute(remote_mirror)
- end
-
- it "does not push anything is remote is up to date" do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
-
- expect(raw_repository).not_to receive(:push_remote_branches)
-
- subject.execute(remote_mirror)
- end
-
- it "sync new branches" do
- # call local_branch_names early so it is not called after the new branch has been created
- current_branches = local_branch_names
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
- create_branch(repository, 'my-new-branch')
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
-
- subject.execute(remote_mirror)
- end
-
- it "sync updated branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
+ expect(remote_mirror).to receive(:update_repository).with({})
subject.execute(remote_mirror)
end
-
- context 'when push only protected branches option is set' do
- let(:unprotected_branch_name) { 'existing-branch' }
- let(:protected_branch_name) do
- project.repository.branch_names.find { |n| n != unprotected_branch_name }
- end
- let!(:protected_branch) do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- before do
- project.reload
- remote_mirror.only_protected_branches = true
- end
-
- it "sync updated protected branches" do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not sync unprotected branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_branch(repository, unprotected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch exists in local and remote repo' do
- context 'when it has diverged' do
- it 'syncs branches' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
- end
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
-
- subject.execute(remote_mirror)
- end
- end
- end
-
- describe 'for delete' do
- context 'when branch exists in local and remote repo' do
- it 'deletes the branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when push only protected branches option is set' do
- before do
- remote_mirror.only_protected_branches = true
- end
-
- context 'when branch exists in local and remote repo' do
- let!(:protected_branch_name) { local_branch_names.first }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- project.reload
- end
-
- it 'deletes the protected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, protected_branch_name)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
-
- it 'does not delete the unprotected branch from remote repo' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
- delete_branch(repository, 'existing-branch')
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when branch only exists on remote repo' do
- let!(:protected_branch_name) { 'remote-branch' }
-
- before do
- create(:protected_branch, project: project, name: protected_branch_name)
- end
-
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
-
- context 'when branch only exists on remote repo' do
- context 'when it has diverged' do
- it 'does not delete the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- rev = repository.find_branch('markdown').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
- end
-
- expect(raw_repository).not_to receive(:delete_remote_branches)
-
- subject.execute(remote_mirror)
- end
- end
-
- context 'when it has not diverged' do
- it 'deletes the remote branch' do
- allow(repository).to receive(:fetch_remote) do
- sync_remote(repository, remote_mirror.remote_name, local_branch_names)
-
- masterrev = repository.find_branch('master').dereferenced_target
- create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
- end
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
-
- subject.execute(remote_mirror)
- end
- end
- end
- end
end
- describe 'Syncing tags' do
- before do
- allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
+ context 'when only syncing protected branches' do
+ let(:unprotected_branch_name) { 'existing-branch' }
+ let(:protected_branch_name) do
+ project.repository.branch_names.find { |n| n != unprotected_branch_name }
end
-
- context 'when there are not tags to push' do
- it 'does not try to push tags' do
- allow(repository).to receive(:remote_tags) { {} }
- allow(repository).to receive(:tags) { [] }
-
- expect(repository).not_to receive(:push_tags)
-
- subject.execute(remote_mirror)
- end
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: protected_branch_name)
end
- context 'when there are some tags to push' do
- it 'pushes tags to remote' do
- allow(raw_repository).to receive(:remote_tags) { {} }
-
- expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
-
- subject.execute(remote_mirror)
- end
+ before do
+ project.reload
+ remote_mirror.only_protected_branches = true
end
- context 'when there are some tags to delete' do
- it 'deletes tags from remote' do
- remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
- allow(raw_repository).to receive(:remote_tags) { remote_tags }
-
- repository.rm_tag(create(:user), 'v1.0.0')
-
- expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
+ it "sync updated protected branches" do
+ allow(repository).to receive(:fetch_remote)
+ expect(remote_mirror).to receive(:update_repository).with(only_branches_matching: [protected_branch_name])
- subject.execute(remote_mirror)
- end
+ subject.execute(remote_mirror)
end
end
end
- def create_branch(repository, branch_name)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target
- parentrev = repository.commit(masterrev).parent_id
-
- rugged.references.create("refs/heads/#{branch_name}", parentrev)
-
- repository.expire_branches_cache
- end
-
- def create_remote_branch(repository, remote_name, branch_name, source_id)
- rugged = repository.rugged
-
- rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
- end
-
def sync_remote(repository, remote_name, local_branch_names)
- rugged = repository.rugged
-
local_branch_names.each do |branch|
- target = repository.find_branch(branch).try(:dereferenced_target)
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
+ commit = repository.commit(branch)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", commit.id) if commit
end
end
def update_remote_branch(repository, remote_name, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
+ masterrev = repository.commit('master').id
- rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
+ repository.write_ref("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
def update_branch(repository, branch)
- rugged = repository.rugged
- masterrev = repository.find_branch('master').dereferenced_target.id
-
- # Updated existing branch
- rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
- repository.expire_branches_cache
- end
-
- def delete_branch(repository, branch)
- rugged = repository.rugged
+ masterrev = repository.commit('master').id
- rugged.references.delete("refs/heads/#{branch}")
+ repository.write_ref("refs/heads/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
index b5fb999381d..812dd42934d 100644
--- a/spec/services/update_merge_request_metrics_service_spec.rb
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -12,7 +12,7 @@ describe MergeRequestMetricsService do
service.merge(event)
expect(metrics.merged_by).to eq(user)
- expect(metrics.merged_at).to eq(event.created_at)
+ expect(metrics.merged_at).to be_like_time(event.created_at)
end
end
@@ -25,7 +25,7 @@ describe MergeRequestMetricsService do
service.close(event)
expect(metrics.latest_closed_by).to eq(user)
- expect(metrics.latest_closed_at).to eq(event.created_at)
+ expect(metrics.latest_closed_at).to be_like_time(event.created_at)
end
end
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 08fd26d67fd..e5fde07a6eb 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
+ include ExclusiveLeaseHelpers
+
# We're using let! here so that any expectations for the service class are not
# triggered twice.
let!(:project) { create(:project) }
@@ -10,12 +12,10 @@ describe Users::RefreshAuthorizedProjectsService do
describe '#execute', :clean_gitlab_redis_shared_state do
it 'refreshes the authorizations using a lease' do
- expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return('foo')
-
- expect(Gitlab::ExclusiveLease).to receive(:cancel)
- .with(an_instance_of(String), 'foo')
+ lease_key = "refresh_authorized_projects:#{user.id}"
+ expect_to_obtain_exclusive_lease(lease_key, 'uuid')
+ expect_to_cancel_exclusive_lease(lease_key, 'uuid')
expect(service).to receive(:execute_without_lease)
service.execute
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 7995f2c9ae7..622e56e1da5 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -60,6 +60,36 @@ describe WebHookService do
).once
end
+ context 'when auth credentials are present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
+
+ it 'uses the credentials' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
+ ).once
+ end
+ end
+
+ context 'when auth credentials are partial present' do
+ let(:url) {'https://example.org'}
+ let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
+
+ it 'uses the credentials anyways' do
+ WebMock.stub_request(:post, url)
+
+ service_instance.execute
+
+ expect(WebMock).to have_requested(:post, url).with(
+ headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
+ ).once
+ end
+ end
+
it 'catches exceptions' do
WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error'))
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
new file mode 100644
index 00000000000..383cc7dee81
--- /dev/null
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -0,0 +1,36 @@
+module ExclusiveLeaseHelpers
+ def stub_exclusive_lease(key = nil, uuid = 'uuid', renew: false, timeout: nil)
+ key ||= instance_of(String)
+ timeout ||= instance_of(Integer)
+
+ lease = instance_double(
+ Gitlab::ExclusiveLease,
+ try_obtain: uuid,
+ exists?: true,
+ renew: renew
+ )
+
+ allow(Gitlab::ExclusiveLease)
+ .to receive(:new)
+ .with(key, timeout: timeout)
+ .and_return(lease)
+
+ lease
+ end
+
+ def stub_exclusive_lease_taken(key = nil, timeout: nil)
+ stub_exclusive_lease(key, nil, timeout: timeout)
+ end
+
+ def expect_to_obtain_exclusive_lease(key, uuid = 'uuid', timeout: nil)
+ lease = stub_exclusive_lease(key, uuid, timeout: timeout)
+
+ expect(lease).to receive(:try_obtain)
+ end
+
+ def expect_to_cancel_exclusive_lease(key, uuid)
+ expect(Gitlab::ExclusiveLease)
+ .to receive(:cancel)
+ .with(key, uuid)
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 329f18cd288..87cfb6c04dc 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -46,8 +46,8 @@ module LoginHelpers
@current_user = user
end
- def gitlab_sign_in_via(provider, user, uid)
- mock_auth_hash(provider, uid, user.email)
+ def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
+ mock_auth_hash(provider, uid, user.email, saml_response)
visit new_user_session_path
click_link provider
end
@@ -87,7 +87,7 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
- def mock_auth_hash(provider, uid, email)
+ def mock_auth_hash(provider, uid, email, saml_response = 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({
@@ -109,12 +109,21 @@ module LoginHelpers
email: email,
image: 'mock_user_thumbnail_url'
}
+ },
+ response_object: {
+ document: saml_xml(saml_response)
}
}
})
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
end
+ def saml_xml(raw_saml_response)
+ return '' if raw_saml_response.blank?
+
+ XMLSecurity::SignedDocument.new(raw_saml_response, [])
+ end
+
def mock_saml_config
OpenStruct.new(name: 'saml', label: 'saml', args: {
assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
@@ -125,6 +134,14 @@ module LoginHelpers
})
end
+ def mock_saml_config_with_upstream_two_factor_authn_contexts
+ config = mock_saml_config
+ config.args[:upstream_two_factor_authn_contexts] = %w(urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN)
+ config
+ end
+
def stub_omniauth_provider(provider, context: Rails.application)
env = env_from_context(context)
@@ -140,13 +157,16 @@ module LoginHelpers
env['omniauth.error.strategy'] = strategy
end
- def stub_omniauth_saml_config(messages)
- set_devise_mapping(context: Rails.application)
- Rails.application.routes.disable_clear_and_finalize = true
- Rails.application.routes.draw do
+ def stub_omniauth_saml_config(messages, context: Rails.application)
+ set_devise_mapping(context: context)
+ routes = Rails.application.routes
+ routes.disable_clear_and_finalize = true
+ routes.formatter.clear
+ routes.draw do
post '/users/auth/saml' => 'omniauth_callbacks#saml'
end
- allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ saml_config = messages.key?(:providers) ? messages[:providers].first : mock_saml_config
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
stub_omniauth_setting(messages)
stub_saml_authorize_path_helpers
end
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index be122f9578c..58b5c6a6435 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -15,9 +15,14 @@ module StubObjectStorage
return unless enabled
+ stub_object_storage(connection_params: uploader.object_store_credentials,
+ remote_directory: remote_directory)
+ end
+
+ def stub_object_storage(connection_params:, remote_directory:)
Fog.mock!
- ::Fog::Storage.new(uploader.object_store_credentials).tap do |connection|
+ ::Fog::Storage.new(connection_params).tap do |connection|
begin
connection.directories.create(key: remote_directory)
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index d23cbaf4beb..be6fa4c71a0 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -7,9 +7,24 @@ RSpec::Matchers.define :require_graphql_authorizations do |*expected|
end
RSpec::Matchers.define :have_graphql_fields do |*expected|
+ def expected_field_names
+ expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
+ end
+
match do |kls|
- field_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) }
- expect(kls.fields.keys).to contain_exactly(*field_names)
+ expect(kls.fields.keys).to contain_exactly(*expected_field_names)
+ end
+
+ failure_message do |kls|
+ missing = expected_field_names - kls.fields.keys
+ extra = kls.fields.keys - expected_field_names
+
+ message = []
+
+ message << "is missing fields: <#{missing.inspect}>" if missing.any?
+ message << "contained unexpected fields: <#{extra.inspect}>" if extra.any?
+
+ message.join("\n")
end
end
@@ -44,3 +59,13 @@ RSpec::Matchers.define :have_graphql_resolver do |expected|
end
end
end
+
+RSpec::Matchers.define :expose_permissions_using do |expected|
+ match do |type|
+ permission_field = type.fields['userPermissions']
+
+ expect(permission_field).not_to be_nil
+ expect(permission_field.type).to be_non_null
+ expect(permission_field.type.of_type.graphql_name).to eq(expected.graphql_name)
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index 6dbe0f6f980..db723a323f8 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -247,8 +247,10 @@ shared_examples_for 'common trace features' do
end
context 'when another process has already been archiving', :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
before do
- Gitlab::ExclusiveLease.new("trace:archive:#{trace.job.id}", timeout: 1.hour).try_obtain
+ stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour)
end
it 'blocks concurrent archiving' do
diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
index 5241c0fa6f1..a8f2c2e7a5a 100644
--- a/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
+++ b/spec/support/shared_examples/features/protected_branches_access_control_ce.rb
@@ -5,6 +5,12 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
within('.js-new-protected-branch') do
allowed_to_push_button = find(".js-allowed-to-push")
@@ -25,6 +31,18 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -59,6 +77,12 @@ shared_examples "protected branches > access control > CE" do
end
end
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -70,6 +94,18 @@ shared_examples "protected branches > access control > CE" do
set_protected_branch_name('master')
+ find(".js-allowed-to-merge").click
+ within('.qa-allowed-to-merge-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
+ find(".js-allowed-to-push").click
+ within('.qa-allowed-to-push-dropdown') do
+ expect(first("li")).to have_content("Roles")
+ find(:link, 'No one').click
+ end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
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 d5e22b8cb56..a401f7541f0 100644
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb
@@ -29,7 +29,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
@@ -53,7 +53,7 @@ shared_examples 'merge requests list' do
expect(response).to include_pagination_headers
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@@ -70,7 +70,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
expect(json_response.last['title']).to eq(merge_request.title)
end
@@ -216,7 +216,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@@ -229,7 +229,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -242,7 +242,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -255,7 +255,7 @@ shared_examples 'merge requests list' do
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(json_response.length).to eq(4)
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@@ -265,7 +265,7 @@ shared_examples 'merge requests list' do
it 'returns merge requests with the given source branch' do
get api(endpoint_path, user), source_branch: merge_request_closed.source_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
@@ -273,7 +273,7 @@ shared_examples 'merge requests list' do
it 'returns merge requests with the given target branch' do
get api(endpoint_path, user), target_branch: merge_request_closed.target_branch, state: 'all'
- expect_response_contain_exactly(merge_request_closed, merge_request_merged)
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
end
end
end
diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb
index 9b2b74593a5..fe7b7bc306f 100644
--- a/spec/support/shared_examples/requests/graphql_shared_examples.rb
+++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
shared_examples 'a working graphql query' do
include GraphqlHelpers
- it 'is returns a successfull response', :aggregate_failures do
- expect(response).to be_success
+ it 'returns a successful response', :aggregate_failures do
+ expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors['errors']).to be_nil
expect(json_response.keys).to include('data')
end
diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb
index 4a25bb9b750..eba990d4037 100644
--- a/spec/support/shared_examples/throttled_touch.rb
+++ b/spec/support/shared_examples/throttled_touch.rb
@@ -3,7 +3,7 @@ shared_examples_for 'throttled touch' do
it 'updates the updated_at timestamp' do
Timecop.freeze do
subject.touch
- expect(subject.updated_at).to eq(Time.zone.now)
+ expect(subject.updated_at).to be_like_time(Time.zone.now)
end
end
@@ -14,7 +14,7 @@ shared_examples_for 'throttled touch' do
Timecop.freeze(first_updated_at) { subject.touch }
Timecop.freeze(second_updated_at) { subject.touch }
- expect(subject.updated_at).to eq(first_updated_at)
+ expect(subject.updated_at).to be_like_time(first_updated_at)
end
end
end
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index 19800c6638f..1bd176280c5 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -76,8 +76,10 @@ shared_examples "migrates" do |to_store:, from_store: nil|
end
context 'when migrate! is occupied by another process' do
+ include ExclusiveLeaseHelpers
+
before do
- @uuid = Gitlab::ExclusiveLease.new(subject.exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+ stub_exclusive_lease_taken(subject.exclusive_lease_key, timeout: 1.hour.to_i)
end
it 'does not execute migrate!' do
@@ -91,10 +93,6 @@ shared_examples "migrates" do |to_store:, from_store: nil|
expect { subject.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken)
end
-
- after do
- Gitlab::ExclusiveLease.cancel(subject.exclusive_lease_key, @uuid)
- end
end
context 'migration is unsuccessful' do
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index fc52c04e78d..b81aea23306 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -20,7 +20,7 @@ describe 'gitlab:db namespace rake task' do
describe 'configure' do
it 'invokes db:migrate when schema has already been loaded' do
- allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(%w[table1 table2])
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
expect(Rake::Task['db:seed_fu']).not_to receive(:invoke)
@@ -35,6 +35,14 @@ describe 'gitlab:db namespace rake task' do
expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
end
+ it 'invokes db:shema:load and db:seed_fu when there is only a single table present' do
+ allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
+ expect(Rake::Task['db:schema:load']).to receive(:invoke)
+ expect(Rake::Task['db:seed_fu']).to receive(:invoke)
+ expect(Rake::Task['db:migrate']).not_to receive(:invoke)
+ expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
+ end
+
it 'does not invoke any other rake tasks during an error' do
allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
diff --git a/spec/workers/delete_diff_files_worker_spec.rb b/spec/workers/delete_diff_files_worker_spec.rb
new file mode 100644
index 00000000000..e0edd313922
--- /dev/null
+++ b/spec/workers/delete_diff_files_worker_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe DeleteDiffFilesWorker do
+ describe '#perform' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_diff) { merge_request.merge_request_diff }
+
+ it 'deletes all merge request diff files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.merge_request_diff_files.count }
+ .from(20).to(0)
+ end
+
+ it 'updates state to without_files' do
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to change { merge_request_diff.reload.state }
+ .from('collected').to('without_files')
+ end
+
+ it 'does nothing if diff was already marked as "without_files"' do
+ merge_request_diff.clean!
+
+ expect_any_instance_of(MergeRequestDiff).not_to receive(:clean!)
+
+ described_class.new.perform(merge_request_diff.id)
+ end
+
+ it 'rollsback if something goes wrong' do
+ expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all)
+ .and_raise
+
+ expect { described_class.new.perform(merge_request_diff.id) }
+ .to raise_error
+
+ merge_request_diff.reload
+
+ expect(merge_request_diff.state).to eq('collected')
+ expect(merge_request_diff.merge_request_diff_files.count).to eq(20)
+ end
+ end
+end
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 36594515005..06d9e125105 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -5,15 +5,17 @@ describe DeleteUserWorker do
let!(:current_user) { create(:user) }
it "calls the DeleteUserWorker with the params it was given" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, {})
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, {})
+ end
described_class.new.perform(current_user.id, user.id)
end
it "uses symbolized keys" do
- expect_any_instance_of(Users::DestroyService).to receive(:execute)
- .with(user, test: "test")
+ expect_next_instance_of(Users::DestroyService) do |service|
+ expect(service).to receive(:execute).with(user, test: "test")
+ end
described_class.new.perform(current_user.id, user.id, "test" => "test")
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 6b1f2ff3227..8c4daac5f80 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -1,49 +1,58 @@
require 'spec_helper'
describe ProjectCacheWorker do
+ include ExclusiveLeaseHelpers
+
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
let(:statistics) { project.statistics }
+ let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
- describe '#perform' do
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
- end
+ before do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ allow(Project).to receive(:find_by)
+ .with(id: project.id)
+ .and_return(project)
+ end
+
+ describe '#perform' do
context 'with a non-existing project' do
- it 'does nothing' do
- expect(worker).not_to receive(:update_statistics)
+ it 'does not update statistic' do
+ allow(Project).to receive(:find_by).with(id: -1).and_return(nil)
- worker.perform(-1)
+ expect(subject).not_to receive(:update_statistics)
+
+ subject.perform(-1)
end
end
context 'with an existing project without a repository' do
- it 'does nothing' do
- allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
+ it 'does not update statistics' do
+ allow(project.repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(subject).not_to receive(:update_statistics)
- worker.perform(project.id)
+ subject.perform(project.id)
end
end
context 'with an existing project' do
it 'updates the project statistics' do
- expect(worker).to receive(:update_statistics)
- .with(kind_of(Project), %i(repository_size))
- .and_call_original
+ expect(subject).to receive(:update_statistics)
+ .with(%w(repository_size))
+ .and_call_original
- worker.perform(project.id, [], %w(repository_size))
+ subject.perform(project.id, [], %w(repository_size))
end
it 'refreshes the method caches' do
- expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
- .and_call_original
+ expect(project.repository).to receive(:refresh_method_caches)
+ .with(%i(readme))
+ .and_call_original
- worker.perform(project.id, %w(readme))
+ subject.perform(project.id, %w(readme))
end
context 'with plain readme' do
@@ -51,39 +60,40 @@ describe ProjectCacheWorker do
allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
allow(MarkupHelper).to receive(:plain?).and_return(true)
- expect_any_instance_of(Repository).to receive(:refresh_method_caches)
- .with(%i(readme))
- .and_call_original
- worker.perform(project.id, %w(readme))
+ expect(project.repository).to receive(:refresh_method_caches)
+ .with(%i(readme))
+ .and_call_original
+
+ subject.perform(project.id, %w(readme))
end
end
end
- end
- describe '#update_statistics' do
context 'when a lease could not be obtained' do
it 'does not update the repository size' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(false)
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(statistics).not_to receive(:refresh!)
+ expect(project.statistics).not_to receive(:refresh!)
- worker.update_statistics(project)
+ subject.perform(project.id, [], %w(repository_size))
end
end
context 'when a lease could be obtained' do
it 'updates the project statistics' do
- allow(worker).to receive(:try_obtain_lease_for)
- .with(project.id, :update_statistics)
- .and_return(true)
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+
+ expect(project.statistics).to receive(:refresh!)
+ .with(only: %i(repository_size))
+ .and_call_original
+
+ subject.perform(project.id, [], %i(repository_size))
+ end
- expect(statistics).to receive(:refresh!)
- .with(only: %i(repository_size))
- .and_call_original
+ it 'cancels the lease after statistics has been updated' do
+ expect(subject).to receive(:release_lease).with('uuid')
- worker.update_statistics(project, %i(repository_size))
+ subject.perform(project.id, [], %i(repository_size))
end
end
end
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
index 2e3951e7afc..9551e358af1 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb
@@ -1,53 +1,47 @@
require 'spec_helper'
describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
describe '#perform' do
let(:project) { create(:project, :empty_repo) }
- let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) }
+ let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" }
+ let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT }
+
+ it 'skips when project no longer exists' do
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+
+ subject.perform(-1)
+ end
- context 'when have exclusive lease' do
- before do
- lease = subject.lease_for(project.id)
+ it 'skips when project is pending delete' do
+ pending_delete_project = create(:project, :empty_repo, pending_delete: true)
- allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
- allow(lease).to receive(:try_obtain).and_return(true)
- end
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- it 'skips when project no longer exists' do
- nonexistent_id = 999999999999
+ subject.perform(pending_delete_project.id)
+ end
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(nonexistent_id)
- end
+ it 'delegates removal to service class when have exclusive lease' do
+ stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout)
- it 'skips when project is pending delete' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ migration_service = spy
- subject.perform(pending_delete_project.id)
- end
+ allow(::Projects::HashedStorageMigrationService)
+ .to receive(:new).with(project, subject.logger)
+ .and_return(migration_service)
- it 'delegates removal to service class' do
- service = double('service')
- expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service)
- expect(service).to receive(:execute)
+ subject.perform(project.id)
- subject.perform(project.id)
- end
+ expect(migration_service).to have_received(:execute)
end
- context 'when dont have exclusive lease' do
- before do
- lease = subject.lease_for(project.id)
-
- allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease)
- allow(lease).to receive(:try_obtain).and_return(false)
- end
+ it 'skips when dont have lease when dont have exclusive lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- it 'skips when dont have lease' do
- expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
+ expect(::Projects::HashedStorageMigrationService).not_to receive(:new)
- subject.perform(project.id)
- end
+ subject.perform(project.id)
end
end
end
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
index b8b65ead9b3..af1fb80a51d 100644
--- a/spec/workers/propagate_service_template_worker_spec.rb
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -1,29 +1,29 @@
require 'spec_helper'
describe PropagateServiceTemplateWorker do
- let!(:service_template) do
- PushoverService.create(
- template: true,
- active: true,
- properties: {
- device: 'MyDevice',
- sound: 'mic',
- priority: 4,
- user_key: 'asdf',
- api_key: '123456789'
- })
- end
-
- before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
- .and_return(true)
- end
+ include ExclusiveLeaseHelpers
describe '#perform' do
it 'calls the propagate service with the template' do
- expect(Projects::PropagateServiceTemplate).to receive(:propagate).with(service_template)
+ template = PushoverService.create(
+ template: true,
+ active: true,
+ properties: {
+ device: 'MyDevice',
+ sound: 'mic',
+ priority: 4,
+ user_key: 'asdf',
+ api_key: '123456789'
+ })
+
+ stub_exclusive_lease("propagate_service_template_worker:#{template.id}",
+ timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT)
+
+ expect(Projects::PropagateServiceTemplate)
+ .to receive(:propagate)
+ .with(template)
- subject.perform(service_template.id)
+ subject.perform(template.id)
end
end
end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 6cd27d2fafb..6bc551be9ad 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -1,14 +1,19 @@
require 'spec_helper'
describe RepositoryCheck::BatchWorker do
+ let(:shard_name) { 'default' }
subject { described_class.new }
+ before do
+ Gitlab::ShardHealthCache.update([shard_name])
+ end
+
it 'prefers projects that have never been checked' do
projects = create_list(:project, 3, created_at: 1.week.ago)
projects[0].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
- expect(subject.perform).to eq(projects.values_at(1, 0, 2).map(&:id))
+ expect(subject.perform(shard_name)).to eq(projects.values_at(1, 0, 2).map(&:id))
end
it 'sorts projects by last_repository_check_at' do
@@ -17,7 +22,7 @@ describe RepositoryCheck::BatchWorker do
projects[1].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
- expect(subject.perform).to eq(projects.values_at(1, 2, 0).map(&:id))
+ expect(subject.perform(shard_name)).to eq(projects.values_at(1, 2, 0).map(&:id))
end
it 'excludes projects that were checked recently' do
@@ -26,7 +31,14 @@ describe RepositoryCheck::BatchWorker do
projects[1].update_column(:last_repository_check_at, 2.months.ago)
projects[2].update_column(:last_repository_check_at, 3.days.ago)
- expect(subject.perform).to eq([projects[1].id])
+ expect(subject.perform(shard_name)).to eq([projects[1].id])
+ end
+
+ it 'excludes projects on another shard' do
+ projects = create_list(:project, 2, created_at: 1.week.ago)
+ projects[0].update_column(:repository_storage, 'other')
+
+ expect(subject.perform(shard_name)).to eq([projects[1].id])
end
it 'does nothing when repository checks are disabled' do
@@ -34,13 +46,20 @@ describe RepositoryCheck::BatchWorker do
stub_application_setting(repository_checks_enabled: false)
- expect(subject.perform).to eq(nil)
+ expect(subject.perform(shard_name)).to eq(nil)
+ end
+
+ it 'does nothing when shard is unhealthy' do
+ shard_name = 'broken'
+ create(:project, created_at: 1.week.ago, repository_storage: shard_name)
+
+ expect(subject.perform(shard_name)).to eq(nil)
end
it 'skips projects created less than 24 hours ago' do
project = create(:project)
project.update_column(:created_at, 23.hours.ago)
- expect(subject.perform).to eq([])
+ expect(subject.perform(shard_name)).to eq([])
end
end
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
new file mode 100644
index 00000000000..20a4f1f5344
--- /dev/null
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe RepositoryCheck::DispatchWorker do
+ subject { described_class.new }
+
+ it 'does nothing when repository checks are disabled' do
+ stub_application_setting(repository_checks_enabled: false)
+
+ expect(RepositoryCheck::BatchWorker).not_to receive(:perform_async)
+
+ subject.perform
+ end
+
+ it 'dispatches work to RepositoryCheck::BatchWorker' do
+ expect(RepositoryCheck::BatchWorker).to receive(:perform_async).at_least(:once)
+
+ subject.perform
+ end
+
+ context 'with unhealthy shard' do
+ let(:default_shard_name) { 'default' }
+ let(:unhealthy_shard_name) { 'unhealthy' }
+ let(:default_shard) { Gitlab::HealthChecks::Result.new(true, nil, shard: default_shard_name) }
+ let(:unhealthy_shard) { Gitlab::HealthChecks::Result.new(false, '14:Connect Failed', shard: unhealthy_shard_name) }
+
+ before do
+ allow(Gitlab::HealthChecks::GitalyCheck).to receive(:readiness).and_return([default_shard, unhealthy_shard])
+ end
+
+ it 'only triggers RepositoryCheck::BatchWorker for healthy shards' do
+ expect(RepositoryCheck::BatchWorker).to receive(:perform_async).with('default')
+
+ subject.perform
+ end
+ end
+end
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
index 5968c5da3c9..a653f6f926c 100644
--- a/spec/workers/repository_remove_remote_worker_spec.rb
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -1,44 +1,50 @@
require 'rails_helper'
describe RepositoryRemoveRemoteWorker do
- subject(:worker) { described_class.new }
+ include ExclusiveLeaseHelpers
describe '#perform' do
- let(:remote_name) { 'joe'}
let!(:project) { create(:project, :repository) }
+ let(:remote_name) { 'joe'}
+ let(:lease_key) { "remove_remote_#{project.id}_#{remote_name}" }
+ let(:lease_timeout) { RepositoryRemoveRemoteWorker::LEASE_TIMEOUT }
- context 'when it cannot obtain lease' do
- it 'logs error' do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
-
- expect_any_instance_of(Repository).not_to receive(:remove_remote)
- expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
-
- worker.perform(project.id, remote_name)
- end
+ it 'returns nil when project does not exist' do
+ expect(subject.perform(-1, 'remote_name')).to be_nil
end
- context 'when it gets the lease' do
+ context 'when project exists' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
+ allow(Project)
+ .to receive(:find_by)
+ .with(id: project.id)
+ .and_return(project)
end
- context 'when project does not exist' do
- it 'returns nil' do
- expect(worker.perform(-1, 'remote_name')).to be_nil
- end
- end
+ it 'does not remove remote when cannot obtain lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+
+ expect(project.repository)
+ .not_to receive(:remove_remote)
- context 'when project exists' do
- it 'removes remote from repository' do
- masterrev = project.repository.find_branch('master').dereferenced_target
+ expect(subject)
+ .to receive(:log_error)
+ .with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
- create_remote_branch(remote_name, 'remote_branch', masterrev)
+ subject.perform(project.id, remote_name)
+ end
- expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
+ it 'removes remote from repository when obtain a lease' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ masterrev = project.repository.find_branch('master').dereferenced_target
+ create_remote_branch(remote_name, 'remote_branch', masterrev)
- worker.perform(project.id, remote_name)
- end
+ expect(project.repository)
+ .to receive(:remove_remote)
+ .with(remote_name)
+ .and_call_original
+
+ subject.perform(project.id, remote_name)
end
end
end
@@ -47,6 +53,7 @@ describe RepositoryRemoveRemoteWorker do
rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
project.repository.rugged
end
+
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index 2605c14334f..856886e3df5 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -1,14 +1,21 @@
require 'spec_helper'
describe StuckCiJobsWorker do
+ include ExclusiveLeaseHelpers
+
let!(:runner) { create :ci_runner }
let!(:job) { create :ci_build, runner: runner }
- let(:worker) { described_class.new }
- let(:exclusive_lease_uuid) { SecureRandom.uuid }
+ let(:trace_lease_key) { "trace:archive:#{job.id}" }
+ let(:trace_lease_uuid) { SecureRandom.uuid }
+ let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY }
+ let(:worker_lease_uuid) { SecureRandom.uuid }
+
+ subject(:worker) { described_class.new }
before do
+ stub_exclusive_lease(worker_lease_key, worker_lease_uuid)
+ stub_exclusive_lease(trace_lease_key, trace_lease_uuid)
job.update!(status: status, updated_at: updated_at)
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end
shared_examples 'job is dropped' do
@@ -44,16 +51,19 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 day ago' do
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 day ago' do
let(:updated_at) { 6.hours.ago }
+
it_behaves_like 'job is unchanged'
end
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -65,11 +75,14 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
- context 'when job was updated in less than 1 hour ago' do
+ context 'when job was updated in less than 1
+ hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -80,11 +93,13 @@ describe StuckCiJobsWorker do
context 'when job was not updated for more than 1 hour ago' do
let(:updated_at) { 2.hours.ago }
+
it_behaves_like 'job is dropped'
end
context 'when job was updated in less than 1 hour ago' do
let(:updated_at) { 30.minutes.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -93,6 +108,7 @@ describe StuckCiJobsWorker do
context "when job is #{status}" do
let(:status) { status }
let(:updated_at) { 2.days.ago }
+
it_behaves_like 'job is unchanged'
end
end
@@ -119,23 +135,27 @@ describe StuckCiJobsWorker do
it 'is guard by exclusive lease when executed concurrently' do
expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).not_to receive(:drop)
+
worker.perform
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
+
+ stub_exclusive_lease_taken(worker_lease_key)
+
worker2.perform
end
it 'can be executed in sequence' do
expect(worker).to receive(:drop).at_least(:once).and_call_original
expect(worker2).to receive(:drop).at_least(:once).and_call_original
+
worker.perform
worker2.perform
end
- it 'cancels exclusive lease after worker perform' do
- worker.perform
+ it 'cancels exclusive leases after worker perform' do
+ expect_to_cancel_exclusive_lease(trace_lease_key, trace_lease_uuid)
+ expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid)
- expect(Gitlab::ExclusiveLease.new(described_class::EXCLUSIVE_LEASE_KEY, timeout: 1.hour))
- .not_to be_exists
+ worker.perform
end
end
end