diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-07-03 13:24:02 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-07-03 13:24:02 +0900 |
commit | f930e94363e00e41fceb31f7d8497e5fb530dacd (patch) | |
tree | bf7129ec74644026bc7662f525ce77ab38acaae8 /spec | |
parent | 58a1a0b70c7df0947864d0be933faf0153b537ec (diff) | |
parent | 275fbf24b1810e2fbef92b6599d5372855b97b46 (diff) | |
download | gitlab-ce-f930e94363e00e41fceb31f7d8497e5fb530dacd.tar.gz |
Merge branch 'master' into build-chunks-on-object-storage
Diffstat (limited to 'spec')
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 Binary files differindex 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 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 Binary files differdeleted file mode 100644 index bef7e2ff8ee..00000000000 --- a/spec/fixtures/exported-project.gz +++ /dev/null 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="  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) { '<img src="x" onerror="alert(42)">' } + 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 |