diff options
Diffstat (limited to 'spec')
72 files changed, 1376 insertions, 820 deletions
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/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/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/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/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/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..c53b6da4b48 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -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..f0bd098f698 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -12,15 +12,16 @@ 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 }], + 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/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..05c6d587e9c 100644 --- a/spec/javascripts/helpers/init_vue_mr_page_helper.js +++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js @@ -6,6 +6,7 @@ import diffFileMockData from '../diffs/mock_data/diff_file'; export default function initVueMRPage() { 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'); @@ -26,6 +27,7 @@ export default function initVueMRPage() { 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); 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..70d13a6ceb2 --- /dev/null +++ b/spec/javascripts/ide/components/error_message_spec.js @@ -0,0 +1,104 @@ +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', () => { + beforeEach(done => { + vm.message.action = 'testAction'; + vm.message.actionText = 'test action'; + vm.message.actionPayload = 'testActionPayload'; + + spyOn(vm.$store, 'dispatch').and.returnValue(Promise.resolve()); + + 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(vm.$store.dispatch).toHaveBeenCalledWith('testAction', 'testActionPayload'); + + done(); + }); + }); + + it('does not dispatch action when already loading', () => { + vm.isLoading = true; + + vm.$el.querySelector('.flash-action').click(); + + expect(vm.$store.dispatch).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/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/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index d71fc0e035e..c1f9fc4f148 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: 'createNewBranchFromDefault', + 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..612a439cfbf 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -1,11 +1,17 @@ +import MockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; +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 +23,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 +38,85 @@ 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(); + }); + }); }); }); @@ -96,6 +140,35 @@ describe('Multi-file store tree actions', () => { }); }); + describe('showTreeEntry', () => { + beforeEach(() => { + const paths = [ + 'grandparent', + 'ancestor', + 'grandparent/parent', + 'grandparent/aunt', + 'grandparent/parent/child.txt', + 'grandparent/aunt/cousing.txt', + ]; + + Object.assign(store.state.entries, createEntriesFromPaths(paths)); + }); + + 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, + ); + }); + }); + describe('getLastCommitData', () => { beforeEach(() => { spyOn(service, 'getTreeLastCommit').and.returnValue( 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/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/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/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/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/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/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/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index ae69a362dda..0d7c930127c 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,14 +330,6 @@ 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 @@ -498,7 +490,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 +508,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/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/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/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/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/repository_spec.rb b/spec/models/repository_spec.rb index d817a8376f4..27a14ff5d5b 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 @@ -664,7 +664,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 +695,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 +711,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 +744,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 +786,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') @@ -2294,6 +2284,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/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/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/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/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/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 790ecce8ded..46e4e3559dc 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -47,5 +47,18 @@ describe MergeRequests::PostMergeService do 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/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index bceaf8277ee..471b0a74a19 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) rescue Excon::Error::Conflict 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/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/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 |
