diff options
Diffstat (limited to 'spec')
53 files changed, 1376 insertions, 294 deletions
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index f677cec3408..b9a979044fe 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -190,10 +190,7 @@ describe Projects::JobsController do expect(response).to have_gitlab_http_status(:ok) expect(json_response['id']).to eq job.id expect(json_response['status']).to eq job.status - end - - it 'returns no job log message' do - expect(json_response['html']).to eq('No job log') + expect(json_response['html']).to be_nil end end diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index c3b71458e38..a102a3a3c8c 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -40,6 +40,30 @@ describe Projects::RepositoriesController do expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") end + it 'handles legacy queries with the ref specified as ref in params' do + get :archive, namespace_id: project.namespace, project_id: project, ref: 'feature', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'handles legacy queries with the ref specified as id in params' do + get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + + it 'prioritizes the id param over the ref param when both are specified' do + get :archive, namespace_id: project.namespace, project_id: project, id: 'feature', ref: 'feature_conflict', format: 'zip' + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:ref)).to eq('feature') + expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-archive:") + end + context "when the service raises an error" do before do allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 6769acb7c9c..e880f0096c1 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -63,6 +63,13 @@ describe 'Issue Boards new issue', :js do page.within(first('.board .issue-count-badge-count')) do expect(page).to have_content('1') end + + page.within(first('.card')) do + issue = project.issues.find_by_title('bug') + + expect(page).to have_content(issue.to_reference) + expect(page).to have_link(issue.title, href: issue_path(issue)) + end end it 'shows sidebar when creating new issue' do diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 8e6493bbd93..4a44ec302fc 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -14,7 +14,7 @@ feature 'Manually create a todo item from issue', :js do it 'creates todo when clicking button' do page.within '.issuable-sidebar' do click_button 'Add todo' - expect(page).to have_content 'Mark done' + expect(page).to have_content 'Mark todo as done' end page.within '.header-content .todos-count' do @@ -31,7 +31,7 @@ feature 'Manually create a todo item from issue', :js do it 'marks a todo as done' do page.within '.issuable-sidebar' do click_button 'Add todo' - click_button 'Mark done' + click_button 'Mark todo as done' end expect(page).to have_selector('.todos-count', visible: false) diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index b4ad4b64d8e..0fd2840c426 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -5,7 +5,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do let(:user) { project.creator } let(:guest) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } - let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) } + let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "| Markdown | Table |\n|-------|---------|\n| first | second |") } let(:path) { "files/ruby/popen.rb" } let(:position) do Gitlab::Diff::Position.new( @@ -111,6 +111,15 @@ describe 'Merge request > User resolves diff notes and discussions', :js do expect(page.find(".line-holder-placeholder")).to be_visible expect(page.find(".timeline-content #note_#{note.id}")).to be_visible end + + it 'renders tables in lazy-loaded resolved diff dicussions' do + find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click + + wait_for_requests + + expect(page.find(".timeline-content #note_#{note.id}")).not_to have_css(".line_holder") + expect(page.find(".timeline-content #note_#{note.id}")).to have_css("tr", count: 2) + end end describe 'side-by-side view' do diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index d96c7e655ba..b242e41df1c 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -44,6 +44,8 @@ feature 'Multi-file editor new directory', :js do wait_for_requests + click_button 'Stage all' + fill_in('commit-message', with: 'commit message ide') click_button('Commit') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index a4cbd5cf766..7d65456e049 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -34,6 +34,8 @@ feature 'Multi-file editor new file', :js do wait_for_requests + click_button 'Stage all' + fill_in('commit-message', with: 'commit message ide') click_button('Commit') diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 4224cea4652..7b59fde999d 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -22,11 +22,15 @@ describe IssuablesHelper do end describe '#issuable_labels_tooltip' do - it 'returns label text' do + it 'returns label text with no labels' do + expect(issuable_labels_tooltip([])).to eq("Labels") + end + + it 'returns label text with labels within max limit' do expect(issuable_labels_tooltip([label])).to eq(label.title) end - it 'returns label text' do + it 'returns label text with labels exceeding max limit' do expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more") end end diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 70b4a89cb86..f5185cb2857 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -83,58 +83,4 @@ describe MilestonesHelper do end end end - - describe '#milestone_remaining_days' do - around do |example| - Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } - end - - context 'when less than 31 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } - - it 'returns days remaining' do - expect(milestone_remaining).to eq("<strong>12</strong> days remaining") - end - end - - context 'when less than 1 year and more than 30 days remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } - - it 'returns months remaining' do - expect(milestone_remaining).to eq("<strong>2</strong> months remaining") - end - end - - context 'when more than 1 year remaining' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } - - it 'returns years remaining' do - expect(milestone_remaining).to eq("<strong>1</strong> year remaining") - end - end - - context 'when milestone is expired' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } - - it 'returns "Past due"' do - expect(milestone_remaining).to eq("<strong>Past due</strong>") - end - end - - context 'when milestone has start_date in the future' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } - - it 'returns "Upcoming"' do - expect(milestone_remaining).to eq("<strong>Upcoming</strong>") - end - end - - context 'when milestone has start_date in the past' do - let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } - - it 'returns days elapsed' do - expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") - end - end - end end diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index 2abf52a1676..8427e8a0ba7 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -85,7 +85,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -97,7 +97,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { setTimeout(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -128,13 +128,13 @@ describe('Issuable right sidebar collapsed todo toggle', () => { .catch(done.fail); }); - it('updates aria-label to mark done', (done) => { + it('updates aria-label to mark todo as done', (done) => { document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); setTimeout(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); done(); }); @@ -147,7 +147,7 @@ describe('Issuable right sidebar collapsed todo toggle', () => { .then(() => { expect( document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'), - ).toBe('Mark done'); + ).toBe('Mark todo as done'); document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click(); }) diff --git a/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js new file mode 100644 index 00000000000..b80d08de7b1 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/empty_state_spec.js @@ -0,0 +1,95 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE commit panel empty state', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(emptyState); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'no-changes', + committedStateSvgPath: 'committed-state', + }); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + describe('statusSvg', () => { + it('uses noChangesStateSvgPath when commit message is empty', () => { + expect(vm.statusSvg).toBe('no-changes'); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe( + 'no-changes', + ); + }); + + it('uses committedStateSvgPath when commit message exists', done => { + vm.$store.state.lastCommitMsg = 'testing'; + + Vue.nextTick(() => { + expect(vm.statusSvg).toBe('committed-state'); + expect(vm.$el.querySelector('img').getAttribute('src')).toBe( + 'committed-state', + ); + + done(); + }); + }); + }); + + it('renders no changes text when last commit message is empty', () => { + expect(vm.$el.textContent).toContain('No changes'); + }); + + it('renders last commit message when it exists', done => { + vm.$store.state.lastCommitMsg = 'testing commit message'; + + Vue.nextTick(() => { + expect(vm.$el.textContent).toContain('testing commit message'); + + done(); + }); + }); + + describe('toggle button', () => { + it('calls store action', () => { + spyOn(vm, 'toggleRightPanelCollapsed'); + + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); + }); + + it('renders collapsed class', done => { + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); + + done(); + }); + }); + }); + + describe('collapsed state', () => { + beforeEach(done => { + vm.$store.state.rightPanelCollapsed = true; + + Vue.nextTick(done); + }); + + it('does not render text & svg', () => { + expect(vm.$el.querySelector('img')).toBeNull(); + expect(vm.$el.textContent).not.toContain('No changes'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js index 32dbc3bf72e..9af3c15a4e3 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js @@ -11,10 +11,17 @@ describe('Multi-file editor commit sidebar list collapsed', () => { beforeEach(() => { const Component = Vue.extend(listCollapsed); - vm = createComponentWithStore(Component, store); - - vm.$store.state.changedFiles.push(file('file1'), file('file2')); - vm.$store.state.changedFiles[0].tempFile = true; + vm = createComponentWithStore(Component, store, { + files: [ + { + ...file('file1'), + tempFile: true, + }, + file('file2'), + ], + iconName: 'staged', + title: 'Staged', + }); vm.$mount(); }); @@ -26,4 +33,40 @@ describe('Multi-file editor commit sidebar list collapsed', () => { it('renders added & modified files count', () => { expect(removeWhitespace(vm.$el.textContent).trim()).toBe('1 1'); }); + + describe('addedFilesLength', () => { + it('returns an length of temp files', () => { + expect(vm.addedFilesLength).toBe(1); + }); + }); + + describe('modifiedFilesLength', () => { + it('returns an length of modified files', () => { + expect(vm.modifiedFilesLength).toBe(1); + }); + }); + + describe('addedFilesIconClass', () => { + it('includes multi-file-addition when addedFiles is not empty', () => { + expect(vm.addedFilesIconClass).toContain('multi-file-addition'); + }); + + it('excludes multi-file-addition when addedFiles is empty', () => { + vm.files = []; + + expect(vm.addedFilesIconClass).not.toContain('multi-file-addition'); + }); + }); + + describe('modifiedFilesClass', () => { + it('includes multi-file-modified when addedFiles is not empty', () => { + expect(vm.modifiedFilesClass).toContain('multi-file-modified'); + }); + + it('excludes multi-file-modified when addedFiles is empty', () => { + vm.files = []; + + expect(vm.modifiedFilesClass).not.toContain('multi-file-modified'); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 509434e4300..cc7e0a3f26d 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; +import store from '~/ide/stores'; import listItem from '~/ide/components/commit_sidebar/list_item.vue'; import router from '~/ide/ide_router'; -import store from '~/ide/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { file, resetStore } from '../../helpers'; @@ -18,6 +18,7 @@ describe('Multi-file editor commit sidebar list item', () => { vm = createComponentWithStore(Component, store, { file: f, + actionComponent: 'stage-button', }).$mount(); }); @@ -31,22 +32,18 @@ describe('Multi-file editor commit sidebar list item', () => { expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); }); - it('calls discardFileChanges when clicking discard button', () => { - spyOn(vm, 'discardFileChanges'); - - vm.$el.querySelector('.multi-file-discard-btn').click(); - - expect(vm.discardFileChanges).toHaveBeenCalled(); + it('renders actionn button', () => { + expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull(); }); it('opens a closed file in the editor when clicking the file path', done => { - spyOn(vm, 'openFileInEditor').and.callThrough(); + spyOn(vm, 'openPendingTab').and.callThrough(); spyOn(router, 'push'); vm.$el.querySelector('.multi-file-commit-list-path').click(); setTimeout(() => { - expect(vm.openFileInEditor).toHaveBeenCalled(); + expect(vm.openPendingTab).toHaveBeenCalled(); expect(router.push).toHaveBeenCalled(); done(); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index a62c0a28340..62fc3f90ad1 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import store from '~/ide/stores'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { file } from '../../helpers'; +import { file, resetStore } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { let vm; @@ -13,6 +13,10 @@ describe('Multi-file editor commit sidebar list', () => { vm = createComponentWithStore(Component, store, { title: 'Staged', fileList: [], + iconName: 'staged', + action: 'stageAllChanges', + actionBtnText: 'stage all', + itemActionComponent: 'stage-button', }); vm.$store.state.rightPanelCollapsed = false; @@ -22,6 +26,8 @@ describe('Multi-file editor commit sidebar list', () => { afterEach(() => { vm.$destroy(); + + resetStore(vm.$store); }); describe('with a list of files', () => { @@ -38,6 +44,12 @@ describe('Multi-file editor commit sidebar list', () => { }); }); + describe('empty files array', () => { + it('renders no changes text when empty', () => { + expect(vm.$el.textContent).toContain('No changes'); + }); + }); + describe('collapsed', () => { beforeEach(done => { vm.$store.state.rightPanelCollapsed = true; @@ -50,4 +62,32 @@ describe('Multi-file editor commit sidebar list', () => { expect(vm.$el.querySelector('.help-block')).toBeNull(); }); }); + + describe('with toggle', () => { + beforeEach(done => { + spyOn(vm, 'toggleRightPanelCollapsed'); + + vm.showToggle = true; + + Vue.nextTick(done); + }); + + it('calls setPanelCollapsedStatus when clickin toggle', () => { + vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click(); + + expect(vm.toggleRightPanelCollapsed).toHaveBeenCalled(); + }); + }); + + describe('action button', () => { + beforeEach(() => { + spyOn(vm, 'stageAllChanges'); + }); + + it('calls store action when clicked', () => { + vm.$el.querySelector('.ide-staged-action-btn').click(); + + expect(vm.stageAllChanges).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js new file mode 100644 index 00000000000..6bf8710bda7 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import stageButton from '~/ide/components/commit_sidebar/stage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE stage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(stageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'stageChange'); + spyOn(vm, 'discardFileChanges'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to discard & stage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(2); + }); + + it('calls store with stage button', () => { + vm.$el.querySelectorAll('.btn')[0].click(); + + expect(vm.stageChange).toHaveBeenCalledWith(f.path); + }); + + it('calls store with discard button', () => { + vm.$el.querySelectorAll('.btn')[1].click(); + + expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js new file mode 100644 index 00000000000..917bbb9fb46 --- /dev/null +++ b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE unstage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(unstageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + spyOn(vm, 'unstageChange'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to unstage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(1); + }); + + it('calls store with unnstage button', () => { + vm.$el.querySelector('.btn').click(); + + expect(vm.unstageChange).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 113ade269e9..768f6e99bf1 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -28,16 +28,34 @@ describe('RepoCommitSection', () => { }, }; + const files = [file('file1'), file('file2')].map(f => + Object.assign(f, { + type: 'blob', + }), + ); + vm.$store.state.rightPanelCollapsed = false; vm.$store.state.currentBranch = 'master'; - vm.$store.state.changedFiles = [file('file1'), file('file2')]; + vm.$store.state.changedFiles = [...files]; vm.$store.state.changedFiles.forEach(f => Object.assign(f, { changed: true, + content: 'changedFile testing', + }), + ); + + vm.$store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }]; + vm.$store.state.stagedFiles.forEach(f => + Object.assign(f, { + changed: true, content: 'testing', }), ); + vm.$store.state.changedFiles.forEach(f => { + vm.$store.state.entries[f.path] = f; + }); + return vm.$mount(); } @@ -94,20 +112,93 @@ describe('RepoCommitSection', () => { ...vm.$el.querySelectorAll('.multi-file-commit-list li'), ]; const submitCommit = vm.$el.querySelector('form .btn'); + const allFiles = vm.$store.state.changedFiles.concat( + vm.$store.state.stagedFiles, + ); expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull(); - expect(changedFileElements.length).toEqual(2); + expect(changedFileElements.length).toEqual(4); changedFileElements.forEach((changedFile, i) => { - expect(changedFile.textContent.trim()).toContain( - vm.$store.state.changedFiles[i].path, - ); + expect(changedFile.textContent.trim()).toContain(allFiles[i].path); }); expect(submitCommit.disabled).toBeTruthy(); expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull(); }); + it('adds changed files into staged files', done => { + vm.$el.querySelector('.ide-staged-action-btn').click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.ide-commit-list-container').textContent, + ).toContain('No changes'); + + done(); + }); + }); + + it('stages a single file', done => { + vm.$el.querySelector('.multi-file-discard-btn .btn').click(); + + Vue.nextTick(() => { + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + + it('discards a single file', done => { + vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelector('.ide-commit-list-container').textContent, + ).not.toContain('file1'); + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + + it('removes all staged files', done => { + vm.$el.querySelectorAll('.ide-staged-action-btn')[1].click(); + + Vue.nextTick(() => { + expect( + vm.$el.querySelectorAll('.ide-commit-list-container')[1].textContent, + ).toContain('No changes'); + + done(); + }); + }); + + it('unstages a single file', done => { + vm.$el + .querySelectorAll('.multi-file-discard-btn')[2] + .querySelector('.btn') + .click(); + + Vue.nextTick(() => { + expect( + vm.$el + .querySelectorAll('.ide-commit-list-container')[1] + .querySelectorAll('li').length, + ).toBe(1); + + done(); + }); + }); + it('updates commitMessage in store on input', done => { const textarea = vm.$el.querySelector('textarea'); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 310d222377f..b06a6c62a1c 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -200,7 +200,7 @@ describe('RepoEditor', () => { vm.setupEditor(); - expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file); + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, null); expect(vm.model).not.toBeNull(); }); @@ -222,7 +222,7 @@ describe('RepoEditor', () => { vm.setupEditor(); expect(vm.editor.onPositionChange).toHaveBeenCalled(); - expect(vm.model.events.size).toBe(1); + expect(vm.model.events.size).toBe(2); }); it('updates state when model content changed', done => { @@ -234,6 +234,20 @@ describe('RepoEditor', () => { done(); }); }); + + it('sets head model as staged file', () => { + spyOn(vm.editor, 'createModel').and.callThrough(); + + Editor.editorInstance.modelManager.dispose(); + + vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' }); + vm.file.staged = true; + vm.file.key = `unstaged-${vm.file.key}`; + + vm.setupEditor(); + + expect(vm.editor.createModel).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]); + }); }); describe('editor updateDimensions', () => { diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js index 8fc2fccb64c..7a6c22b6d27 100644 --- a/spec/javascripts/ide/lib/common/model_spec.js +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -30,6 +30,19 @@ describe('Multi-file editor library model', () => { expect(model.baseModel).not.toBeNull(); }); + it('creates model with head file to compare against', () => { + const f = file('path'); + model.dispose(); + + model = new Model(monaco, f, { + ...f, + content: '123 testing', + }); + + expect(model.head).not.toBeNull(); + expect(model.getOriginalModel().getValue()).toBe('123 testing'); + }); + it('adds eventHub listener', () => { expect(eventHub.$on).toHaveBeenCalledWith( `editor.update.model.dispose.${model.file.key}`, @@ -70,13 +83,6 @@ describe('Multi-file editor library model', () => { }); describe('onChange', () => { - it('caches event by path', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - expect(model.events.keys().next().value).toBe(model.file.key); - }); - it('calls callback on change', done => { const spy = jasmine.createSpy(); model.onChange(spy); @@ -119,5 +125,15 @@ describe('Multi-file editor library model', () => { jasmine.anything(), ); }); + + it('calls onDispose callback', () => { + const disposeSpy = jasmine.createSpy(); + + model.onDispose(disposeSpy); + + model.dispose(); + + expect(disposeSpy).toHaveBeenCalled(); + }); }); }); diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js index aec325e26a9..e1c4ca570b6 100644 --- a/spec/javascripts/ide/lib/decorations/controller_spec.js +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -117,4 +117,33 @@ describe('Multi-file editor library decorations controller', () => { expect(controller.editorDecorations.size).toBe(0); }); }); + + describe('hasDecorations', () => { + it('returns true when decorations are cached', () => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + + expect(controller.hasDecorations(model)).toBe(true); + }); + + it('returns false when no model decorations exist', () => { + expect(controller.hasDecorations(model)).toBe(false); + }); + }); + + describe('removeDecorations', () => { + beforeEach(() => { + controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); + controller.decorate(model); + }); + + it('removes cached decorations', () => { + expect(controller.decorations.size).not.toBe(0); + expect(controller.editorDecorations.size).not.toBe(0); + + controller.removeDecorations(model); + + expect(controller.decorations.size).toBe(0); + expect(controller.editorDecorations.size).toBe(0); + }); + }); }); diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js index ff73240734e..fd8ab3b4f1d 100644 --- a/spec/javascripts/ide/lib/diff/controller_spec.js +++ b/spec/javascripts/ide/lib/diff/controller_spec.js @@ -3,10 +3,7 @@ import monacoLoader from '~/ide/monaco_loader'; import editor from '~/ide/lib/editor'; import ModelManager from '~/ide/lib/common/model_manager'; import DecorationsController from '~/ide/lib/decorations/controller'; -import DirtyDiffController, { - getDiffChangeType, - getDecorator, -} from '~/ide/lib/diff/controller'; +import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; import { computeDiff } from '~/ide/lib/diff/diff'; import { file } from '../../helpers'; @@ -90,6 +87,14 @@ describe('Multi-file editor library dirty diff controller', () => { expect(model.onChange).toHaveBeenCalled(); }); + it('adds dispose event callback', () => { + spyOn(model, 'onDispose'); + + controller.attachModel(model); + + expect(model.onDispose).toHaveBeenCalled(); + }); + it('calls throttledComputeDiff on change', () => { spyOn(controller, 'throttledComputeDiff'); @@ -99,6 +104,12 @@ describe('Multi-file editor library dirty diff controller', () => { expect(controller.throttledComputeDiff).toHaveBeenCalled(); }); + + it('caches model', () => { + controller.attachModel(model); + + expect(controller.models.has(model.url)).toBe(true); + }); }); describe('computeDiff', () => { @@ -116,14 +127,22 @@ describe('Multi-file editor library dirty diff controller', () => { }); describe('reDecorate', () => { - it('calls decorations controller decorate', () => { + it('calls computeDiff when no decorations are cached', () => { + spyOn(controller, 'computeDiff'); + + controller.reDecorate(model); + + expect(controller.computeDiff).toHaveBeenCalledWith(model); + }); + + it('calls decorate when decorations are cached', () => { spyOn(controller.decorationsController, 'decorate'); + controller.decorationsController.decorations.set(model.url, 'test'); + controller.reDecorate(model); - expect(controller.decorationsController.decorate).toHaveBeenCalledWith( - model, - ); + expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); }); }); @@ -133,16 +152,15 @@ describe('Multi-file editor library dirty diff controller', () => { controller.decorate({ data: { changes: [], path: model.path } }); - expect( - controller.decorationsController.addDecorations, - ).toHaveBeenCalledWith(model, 'dirtyDiff', jasmine.anything()); + expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith( + model, + 'dirtyDiff', + jasmine.anything(), + ); }); it('adds decorations into editor', () => { - const spy = spyOn( - controller.decorationsController.editor.instance, - 'deltaDecorations', - ); + const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); controller.decorate({ data: { changes: computeDiff('123', '1234'), path: model.path }, @@ -181,16 +199,22 @@ describe('Multi-file editor library dirty diff controller', () => { }); it('removes worker event listener', () => { - spyOn( - controller.dirtyDiffWorker, - 'removeEventListener', - ).and.callThrough(); + spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough(); controller.dispose(); - expect( - controller.dirtyDiffWorker.removeEventListener, - ).toHaveBeenCalledWith('message', jasmine.anything()); + expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith( + 'message', + jasmine.anything(), + ); + }); + + it('clears cached models', () => { + controller.attachModel(model); + + model.dispose(); + + expect(controller.models.size).toBe(0); }); }); }); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index 75e6f0f54ec..530bdfa2759 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -88,7 +88,7 @@ describe('Multi-file editor library', () => { instance.createModel('FILE'); - expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE'); + expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null); }); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 479ed7ce49e..ce5c525bed7 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -1,9 +1,12 @@ import Vue from 'vue'; import store from '~/ide/stores'; +import * as actions from '~/ide/stores/actions/file'; +import * as types from '~/ide/stores/mutation_types'; import service from '~/ide/services'; import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; import { file, resetStore } from '../../helpers'; +import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store file actions', () => { beforeEach(() => { @@ -402,6 +405,7 @@ describe('IDE store file actions', () => { beforeEach(() => { spyOn(eventHub, '$on'); + spyOn(eventHub, '$emit'); tmpFile = file(); tmpFile.content = 'testing'; @@ -460,6 +464,57 @@ describe('IDE store file actions', () => { }) .catch(done.fail); }); + + it('pushes route for active file', done => { + tmpFile.active = true; + store.state.openFiles.push(tmpFile); + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`); + + done(); + }) + .catch(done.fail); + }); + + it('emits eventHub event to dispose cached model', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('stageChange', () => { + it('calls STAGE_CHANGE with file path', done => { + testAction( + actions.stageChange, + 'path', + store.state, + [{ type: types.STAGE_CHANGE, payload: 'path' }], + [], + done, + ); + }); + }); + + describe('unstageChange', () => { + it('calls UNSTAGE_CHANGE with file path', done => { + testAction( + actions.unstageChange, + 'path', + store.state, + [{ type: types.UNSTAGE_CHANGE, payload: 'path' }], + [], + done, + ); + }); }); describe('openPendingTab', () => { @@ -476,7 +531,7 @@ describe('IDE store file actions', () => { it('makes file pending in openFiles', done => { store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(store.state.openFiles[0].pending).toBe(true); }) @@ -486,7 +541,7 @@ describe('IDE store file actions', () => { it('returns true when opened', done => { store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(added => { expect(added).toBe(true); }) @@ -498,7 +553,7 @@ describe('IDE store file actions', () => { store.state.currentBranchId = 'master'; store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/'); }) @@ -512,7 +567,7 @@ describe('IDE store file actions', () => { store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(() => { expect(scrollToTabSpy).toHaveBeenCalled(); store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line @@ -527,7 +582,7 @@ describe('IDE store file actions', () => { store.state.viewer = 'diff'; store - .dispatch('openPendingTab', f) + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) .then(added => { expect(added).toBe(false); }) diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index cec572f4507..22a7441ba92 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -1,7 +1,10 @@ import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/ide/stores'; +import * as actions from '~/ide/stores/actions'; +import * as types from '~/ide/stores/mutation_types'; import router from '~/ide/ide_router'; import { resetStore, file } from '../helpers'; +import testAction from '../../helpers/vuex_action_helper'; describe('Multi-file store actions', () => { beforeEach(() => { @@ -191,9 +194,7 @@ describe('Multi-file store actions', () => { }) .then(f => { expect(f.tempFile).toBeTruthy(); - expect(store.state.trees['abcproject/mybranch'].tree.length).toBe( - 1, - ); + expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); done(); }) @@ -292,6 +293,42 @@ describe('Multi-file store actions', () => { }); }); + describe('stageAllChanges', () => { + it('adds all files from changedFiles to stagedFiles', done => { + store.state.changedFiles.push(file(), file('new')); + + testAction( + actions.stageAllChanges, + null, + store.state, + [ + { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, + { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, + ], + [], + done, + ); + }); + }); + + describe('unstageAllChanges', () => { + it('removes all files from stagedFiles after unstaging', done => { + store.state.stagedFiles.push(file(), file('new')); + + testAction( + actions.unstageAllChanges, + null, + store.state, + [ + { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, + { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, + ], + [], + done, + ); + }); + }); + describe('updateViewer', () => { it('updates viewer state', done => { store diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 33733b97dff..8d04b83928c 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -37,19 +37,11 @@ describe('IDE store getters', () => { expect(modifiedFiles.length).toBe(1); expect(modifiedFiles[0].name).toBe('changed'); }); - }); - describe('addedFiles', () => { - it('returns a list of added files', () => { - localState.openFiles.push(file()); - localState.changedFiles.push(file('added')); - localState.changedFiles[0].changed = true; - localState.changedFiles[0].tempFile = true; + it('returns angle left when collapsed', () => { + localState.rightPanelCollapsed = true; - const modifiedFiles = getters.addedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('added'); + expect(getters.collapseButtonIcon(localState)).toBe('angle-double-left'); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 1946a0c547c..116967208e0 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -209,14 +209,14 @@ describe('IDE commit module actions', () => { }, }, }; - store.state.changedFiles.push(f, { + store.state.stagedFiles.push(f, { ...file('changedFile2'), changed: true, }); - store.state.openFiles = store.state.changedFiles; + store.state.openFiles = store.state.stagedFiles; - store.state.changedFiles.forEach(changedFile => { - store.state.entries[changedFile.path] = changedFile; + store.state.stagedFiles.forEach(stagedFile => { + store.state.entries[stagedFile.path] = stagedFile; }); }); @@ -248,19 +248,6 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('removes all changed files', done => { - store - .dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }) - .then(() => { - expect(store.state.changedFiles.length).toBe(0); - }) - .then(done) - .catch(done.fail); - }); - it('sets files commit data', done => { store .dispatch('commit/updateFilesAfterCommit', { @@ -294,10 +281,10 @@ describe('IDE commit module actions', () => { branch, }) .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith( - `editor.update.model.content.${f.path}`, - f.content, - ); + expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, { + content: f.content, + changed: false, + }); }) .then(done) .catch(done.fail); @@ -335,12 +322,22 @@ describe('IDE commit module actions', () => { }, }, }; - store.state.changedFiles.push(file('changed')); - store.state.changedFiles[0].active = true; + + const f = { + ...file('changed'), + type: 'blob', + active: true, + }; + store.state.stagedFiles.push(f); + store.state.changedFiles = [ + { + ...f, + }, + ]; store.state.openFiles = store.state.changedFiles; - store.state.openFiles.forEach(f => { - store.state.entries[f.path] = f; + store.state.openFiles.forEach(localF => { + store.state.entries[localF.path] = localF; }); store.state.commit.commitAction = '2'; @@ -420,11 +417,13 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('adds commit data to changed files', done => { + it('adds commit data to files', done => { store .dispatch('commit/commitChanges') .then(() => { - expect(store.state.openFiles[0].lastCommit.message).toBe('test message'); + expect(store.state.entries[store.state.openFiles[0].path].lastCommit.message).toBe( + 'test message', + ); done(); }) @@ -443,6 +442,16 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); + it('removes all staged files', done => { + store + .dispatch('commit/commitChanges') + .then(() => { + expect(store.state.stagedFiles.length).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + describe('merge request', () => { it('redirects to new merge request page', done => { spyOn(eventHub, '$on'); @@ -471,7 +480,7 @@ describe('IDE commit module actions', () => { store .dispatch('commit/commitChanges') .then(() => { - expect(store.state.changedFiles.length).toBe(0); + expect(store.state.stagedFiles.length).toBe(0); done(); }) diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index e396284ec2c..55580f046ad 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -34,17 +34,17 @@ describe('IDE commit module getters', () => { discardDraftButtonDisabled: false, }; const rootState = { - changedFiles: ['a'], + stagedFiles: ['a'], }; - it('returns false when discardDraftButtonDisabled is false & changedFiles is not empty', () => { + 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 & changedFiles is empty', () => { - rootState.changedFiles.length = 0; + it('returns true when discardDraftButtonDisabled is false & stagedFiles is empty', () => { + rootState.stagedFiles.length = 0; expect( getters.commitButtonDisabled(state, localGetters, rootState), @@ -61,7 +61,7 @@ describe('IDE commit module getters', () => { it('returns true when discardDraftButtonDisabled is false & changedFiles is not empty', () => { localGetters.discardDraftButtonDisabled = false; - rootState.changedFiles.length = 0; + rootState.stagedFiles.length = 0; expect( getters.commitButtonDisabled(state, localGetters, rootState), diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index bf9d5166d0a..6fba934810d 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -8,7 +8,10 @@ describe('IDE store file mutations', () => { beforeEach(() => { localState = state(); - localFile = file(); + localFile = { + ...file(), + type: 'blob', + }; localState.entries[localFile.path] = localFile; }); @@ -183,6 +186,49 @@ describe('IDE store file mutations', () => { }); }); + describe('STAGE_CHANGE', () => { + it('adds file into stagedFiles array', () => { + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0]).toEqual(localFile); + }); + + it('updates stagedFile if it is already staged', () => { + mutations.STAGE_CHANGE(localState, localFile.path); + + localFile.raw = 'testing 123'; + + mutations.STAGE_CHANGE(localState, localFile.path); + + expect(localState.stagedFiles.length).toBe(1); + expect(localState.stagedFiles[0].raw).toEqual('testing 123'); + }); + }); + + describe('UNSTAGE_CHANGE', () => { + let f; + + beforeEach(() => { + f = { + ...file(), + type: 'blob', + staged: true, + }; + + localState.stagedFiles.push(f); + localState.changedFiles.push(f); + localState.entries[f.path] = f; + }); + + it('removes from stagedFiles array', () => { + mutations.UNSTAGE_CHANGE(localState, f.path); + + expect(localState.stagedFiles.length).toBe(0); + expect(localState.changedFiles.length).toBe(1); + }); + }); + describe('TOGGLE_FILE_CHANGED', () => { it('updates file changed status', () => { mutations.TOGGLE_FILE_CHANGED(localState, { diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 38162a470ad..26e7ed4535e 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -69,6 +69,16 @@ describe('Multi-file store mutations', () => { }); }); + describe('CLEAR_STAGED_CHANGES', () => { + it('clears stagedFiles array', () => { + localState.stagedFiles.push('a'); + + mutations.CLEAR_STAGED_CHANGES(localState); + + expect(localState.stagedFiles.length).toBe(0); + }); + }); + describe('UPDATE_VIEWER', () => { it('sets viewer state', () => { mutations.UPDATE_VIEWER(localState, 'diff'); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index f37426a72d4..047ecab27db 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -92,6 +92,7 @@ describe('Issue', function() { function mockCanCreateBranch(canCreateBranch) { mock.onGet(/(.*)\/can_create_branch$/).reply(200, { can_create_branch: canCreateBranch, + suggested_branch_name: 'foo-99', }); } diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index 074323d47d2..ecd8657c406 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -156,4 +156,15 @@ describe Gitlab::GitalyClient::RepositoryService do client.calculate_checksum end end + + describe '#create_from_snapshot' do + it 'sends a create_repository_from_snapshot message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:create_repository_from_snapshot) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double) + + client.create_from_snapshot('http://example.com?wiki=1', 'Custom xyz') + end + end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f84a777a27f..05790bb5fe1 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -537,12 +537,6 @@ ProjectCustomAttribute: - project_id - key - value -LfsFileLock: -- id -- path -- user_id -- project_id -- created_at Badge: - id - link_url diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index d64ea72e346..e732b089d44 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -482,4 +482,26 @@ describe Gitlab::Workhorse do }.deep_stringify_keys) end end + + describe '.send_git_snapshot' do + let(:url) { 'http://example.com' } + + subject(:request) { described_class.send_git_snapshot(repository) } + + it 'sets the header correctly' do + key, command, params = decode_workhorse_header(request) + + expect(key).to eq("Gitlab-Workhorse-Send-Data") + expect(command).to eq('git-snapshot') + expect(params).to eq( + 'GitalyServer' => { + 'address' => Gitlab::GitalyClient.address(project.repository_storage), + 'token' => Gitlab::GitalyClient.token(project.repository_storage) + }, + 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new( + repository: repository.gitaly_repository + ).to_json + ) + end + end end diff --git a/spec/migrations/create_missing_namespace_for_internal_users_spec.rb b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb new file mode 100644 index 00000000000..ac3a4b1f68f --- /dev/null +++ b/spec/migrations/create_missing_namespace_for_internal_users_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb') + +describe CreateMissingNamespaceForInternalUsers, :migration do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:routes) { table(:routes) } + + internal_user_types = [:ghost] + internal_user_types << :support_bot if ActiveRecord::Base.connection.column_exists?(:users, :support_bot) + + internal_user_types.each do |attr| + context "for #{attr} user" do + let(:internal_user) do + users.create!(email: 'test@example.com', projects_limit: 100, username: 'test', attr => true) + end + + it 'creates the missing namespace' do + expect(namespaces.find_by(owner_id: internal_user.id)).to be_nil + + migrate! + + namespace = Namespace.find_by(type: nil, owner_id: internal_user.id) + route = namespace.route + + expect(namespace.path).to eq(route.path) + expect(namespace.name).to eq(route.name) + end + + it 'sets notification email' do + users.update(internal_user.id, notification_email: nil) + + expect(users.find(internal_user.id).notification_email).to be_nil + + migrate! + + user = users.find(internal_user.id) + expect(user.notification_email).to eq(user.email) + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 451b038e9a6..fcdc31c8984 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1384,29 +1384,51 @@ describe Ci::Build do end end - describe '#update_project_statistics' do - let!(:build) { create(:ci_build, artifacts_size: 23) } - - it 'updates project statistics when the artifact size changes' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(build.project_id, [], [:build_artifacts_size]) + context 'when updating the build' do + let(:build) { create(:ci_build, artifacts_size: 23) } + it 'updates project statistics' do build.artifacts_size = 42 - build.save! + + expect(build).to receive(:update_project_statistics_after_save).and_call_original + + expect { build.save! } + .to change { build.project.statistics.reload.build_artifacts_size } + .by(19) end - it 'does not update project statistics when the artifact size stays the same' do - expect(ProjectCacheWorker).not_to receive(:perform_async) + context 'when the artifact size stays the same' do + it 'does not update project statistics' do + build.name = 'changed' - build.name = 'changed' - build.save! + expect(build).not_to receive(:update_project_statistics_after_save) + + build.save! + end end + end - it 'updates project statistics when the build is destroyed' do - expect(ProjectCacheWorker).to receive(:perform_async) - .with(build.project_id, [], [:build_artifacts_size]) + context 'when destroying the build' do + let!(:build) { create(:ci_build, artifacts_size: 23) } + + it 'updates project statistics' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original + + expect { build.destroy! } + .to change { build.project.statistics.reload.build_artifacts_size } + .by(-23) + end + + context 'when the build is destroyed due to the project being destroyed' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) - build.destroy + build.project.update_attributes(pending_delete: true) + build.project.destroy! + end end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 1aa28434879..a3e119cbc27 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Ci::JobArtifact do - set(:artifact) { create(:ci_job_artifact, :archive) } + let(:artifact) { create(:ci_job_artifact, :archive) } describe "Associations" do it { is_expected.to belong_to(:project) } @@ -59,10 +59,32 @@ describe Ci::JobArtifact do end end - describe '#set_size' do - it 'sets the size' do + context 'creating the artifact' do + let(:project) { create(:project) } + let(:artifact) { create(:ci_job_artifact, :archive, project: project) } + + it 'sets the size from the file size' do expect(artifact.size).to eq(106365) end + + it 'updates the project statistics' do + expect { artifact } + .to change { project.statistics.reload.build_artifacts_size } + .by(106365) + end + end + + context 'updating the artifact file' do + it 'updates the artifact size' do + artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) + expect(artifact.size).to eq(1062) + end + + it 'updates the project statistics' do + expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + .to change { artifact.project.statistics.reload.build_artifacts_size } + .by(1062 - 106365) + end end describe '#file' do @@ -118,4 +140,71 @@ describe Ci::JobArtifact do is_expected.to be_nil end end + + context 'when destroying the artifact' do + let(:project) { create(:project, :repository) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + it 'updates the project statistics' do + artifact = build.job_artifacts.first + + expect(ProjectStatistics) + .to receive(:increment_statistic) + .and_call_original + + expect { artifact.destroy } + .to change { project.statistics.reload.build_artifacts_size } + .by(-106365) + end + + context 'when it is destroyed from the project level' do + it 'does not update the project statistics' do + expect(ProjectStatistics) + .not_to receive(:increment_statistic) + + project.update_attributes(pending_delete: true) + project.destroy! + end + end + end + + describe 'file is being stored' do + subject { create(:ci_job_artifact, :archive) } + + context 'when object has nil store' do + before do + subject.update_column(:file_store, nil) + subject.reload + end + + it 'is stored locally' do + expect(subject.file_store).to be(nil) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when existing object has local store' do + it 'is stored locally' do + expect(subject.file_store).to be(ObjectStorage::Store::LOCAL) + expect(subject.file).to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when direct upload is enabled' do + before do + stub_artifacts_object_storage(direct_upload: true) + end + + context 'when file is stored' do + it 'is stored remotely' do + expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(subject.file).not_to be_file_storage + expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end + end + end + end end diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb index 914730718e7..6cd2de6dcce 100644 --- a/spec/models/concerns/uniquify_spec.rb +++ b/spec/models/concerns/uniquify_spec.rb @@ -22,6 +22,15 @@ describe Uniquify do expect(result).to eq('test_string2') end + it 'allows to pass an initial value for the counter' do + start_counting_from = 2 + uniquify = described_class.new(start_counting_from) + + result = uniquify.string('test_string') { |s| s == 'test_string' } + + expect(result).to eq('test_string2') + end + it 'allows passing in a base function that defines the location of the counter' do result = uniquify.string(-> (counter) { "test_#{counter}_string" }) do |s| s == 'test__string' diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index ac30cd27e0c..aee70bcfb29 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -16,6 +16,15 @@ describe Deployment do it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } + describe 'modules' do + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:deployment) } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :deployments } + end + end + describe 'after_create callbacks' do let(:environment) { create(:environment) } let(:store) { Gitlab::EtagCaching::Store.new } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 11154291368..128acf83686 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -376,6 +376,48 @@ describe Issue do end end + describe '#suggested_branch_name' do + let(:repository) { double } + + subject { build(:issue) } + + before do + allow(subject.project).to receive(:repository).and_return(repository) + end + + context '#to_branch_name does not exists' do + before do + allow(repository).to receive(:branch_exists?).and_return(false) + end + + it 'returns #to_branch_name' do + expect(subject.suggested_branch_name).to eq(subject.to_branch_name) + end + end + + context '#to_branch_name exists not ending with -index' do + before do + allow(repository).to receive(:branch_exists?).and_return(true) + allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(false) + end + + it 'returns #to_branch_name ending with -2' do + expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-2") + end + end + + context '#to_branch_name exists ending with -index' do + before do + allow(repository).to receive(:branch_exists?).and_return(true) + allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false) + end + + it 'returns #to_branch_name ending with max index + 1' do + expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3") + end + end + end + describe '#has_related_branch?' do let(:issue) { create(:issue, title: "Blue Bell Knoll") } subject { issue.has_related_branch? } @@ -425,6 +467,27 @@ describe Issue do end end + describe '#can_be_worked_on?' do + let(:project) { build(:project) } + subject { build(:issue, :opened, project: project) } + + context 'is closed' do + subject { build(:issue, :closed) } + + it { is_expected.not_to be_can_be_worked_on } + end + + context 'project is forked' do + before do + allow(project).to receive(:forked?).and_return(true) + end + + it { is_expected.not_to be_can_be_worked_on } + end + + it { is_expected.to be_can_be_worked_on } + end + describe '#participants' do context 'using a public project' do let(:project) { create(:project, :public) } diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb index a182116d637..ba06ff42d87 100644 --- a/spec/models/lfs_object_spec.rb +++ b/spec/models/lfs_object_spec.rb @@ -81,5 +81,44 @@ describe LfsObject do end end end + + describe 'file is being stored' do + let(:lfs_object) { create(:lfs_object, :with_file) } + + context 'when object has nil store' do + before do + lfs_object.update_column(:file_store, nil) + lfs_object.reload + end + + it 'is stored locally' do + expect(lfs_object.file_store).to be(nil) + expect(lfs_object.file).to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when existing object has local store' do + it 'is stored locally' do + expect(lfs_object.file_store).to be(ObjectStorage::Store::LOCAL) + expect(lfs_object.file).to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::LOCAL) + end + end + + context 'when direct upload is enabled' do + before do + stub_lfs_object_storage(direct_upload: true) + end + + context 'when file is stored' do + it 'is stored remotely' do + expect(lfs_object.file_store).to eq(ObjectStorage::Store::REMOTE) + expect(lfs_object.file).not_to be_file_storage + expect(lfs_object.file.object_store).to eq(ObjectStorage::Store::REMOTE) + end + end + end + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f73f44ca0ad..becb146422e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -17,11 +17,17 @@ describe MergeRequest do describe 'modules' do subject { described_class } - it { is_expected.to include_module(NonatomicInternalId) } it { is_expected.to include_module(Issuable) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } it { is_expected.to include_module(Taskable) } + + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:merge_request) } + let(:scope_attrs) { { project: instance.target_project } } + let(:usage) { :merge_requests } + end end describe 'validation' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 47f4a792e5c..4bb9717d33e 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -1,6 +1,26 @@ require 'spec_helper' describe Milestone do + describe 'modules' do + context 'with a project' do + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:milestone, project: build(:project), group: nil) } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :milestones } + end + end + + context 'with a group' do + it_behaves_like 'AtomicInternalId' do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:milestone, project: nil, group: build(:group)) } + let(:scope_attrs) { { namespace: instance.group } } + let(:usage) { :milestones } + end + end + end + describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) @@ -96,7 +116,9 @@ describe Milestone do allow(milestone).to receive(:due_date).and_return(Date.today.prev_year) end - it { expect(milestone.expired?).to be_truthy } + it 'returns true when due_date is in the past' do + expect(milestone.expired?).to be_truthy + end end context "not expired" do @@ -104,17 +126,19 @@ describe Milestone do allow(milestone).to receive(:due_date).and_return(Date.today.next_year) end - it { expect(milestone.expired?).to be_falsey } + it 'returns false when due_date is in the future' do + expect(milestone.expired?).to be_falsey + end end end describe '#upcoming?' do - it 'returns true' do + it 'returns true when start_date is in the future' do milestone = build(:milestone, start_date: Time.now + 1.month) expect(milestone.upcoming?).to be_truthy end - it 'returns false' do + it 'returns false when start_date is in the past' do milestone = build(:milestone, start_date: Date.today.prev_year) expect(milestone.upcoming?).to be_falsey end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 86962cd8d61..6a6c71e6c82 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -91,6 +91,23 @@ describe Note do it "keeps the commit around" do expect(note.project.repository.kept_around?(commit.id)).to be_truthy end + + it 'does not generate N+1 queries for participants', :request_store do + def retrieve_participants + commit.notes_with_associations.map(&:participants).to_a + end + + # Project authorization checks are cached, establish a baseline + retrieve_participants + + control_count = ActiveRecord::QueryRecorder.new do + retrieve_participants + end + + create(:note_on_commit, project: note.project, note: 'another note', noteable_id: commit.id) + + expect { retrieve_participants }.not_to exceed_query_limit(control_count) + end end describe 'authorization' do diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index 5cff2af4aca..38a3590ad12 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -4,26 +4,6 @@ describe ProjectStatistics do let(:project) { create :project } let(:statistics) { project.statistics } - describe 'constants' do - describe 'STORAGE_COLUMNS' do - it 'is an array of symbols' do - expect(described_class::STORAGE_COLUMNS).to be_kind_of Array - expect(described_class::STORAGE_COLUMNS.map(&:class).uniq).to eq [Symbol] - end - end - - describe 'STATISTICS_COLUMNS' do - it 'is an array of symbols' do - expect(described_class::STATISTICS_COLUMNS).to be_kind_of Array - expect(described_class::STATISTICS_COLUMNS.map(&:class).uniq).to eq [Symbol] - end - - it 'includes all storage columns' do - expect(described_class::STATISTICS_COLUMNS & described_class::STORAGE_COLUMNS).to eq described_class::STORAGE_COLUMNS - end - end - end - describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:namespace) } @@ -63,7 +43,6 @@ describe ProjectStatistics do allow(statistics).to receive(:update_commit_count) allow(statistics).to receive(:update_repository_size) allow(statistics).to receive(:update_lfs_objects_size) - allow(statistics).to receive(:update_build_artifacts_size) allow(statistics).to receive(:update_storage_size) end @@ -76,7 +55,6 @@ describe ProjectStatistics do expect(statistics).to have_received(:update_commit_count) expect(statistics).to have_received(:update_repository_size) expect(statistics).to have_received(:update_lfs_objects_size) - expect(statistics).to have_received(:update_build_artifacts_size) end end @@ -89,7 +67,6 @@ describe ProjectStatistics do expect(statistics).to have_received(:update_lfs_objects_size) expect(statistics).not_to have_received(:update_commit_count) expect(statistics).not_to have_received(:update_repository_size) - expect(statistics).not_to have_received(:update_build_artifacts_size) end end end @@ -131,40 +108,6 @@ describe ProjectStatistics do end end - describe '#update_build_artifacts_size' do - let!(:pipeline) { create(:ci_pipeline, project: project) } - - context 'when new job artifacts are calculated' do - let(:ci_build) { create(:ci_build, pipeline: pipeline) } - - before do - create(:ci_job_artifact, :archive, project: pipeline.project, job: ci_build) - end - - it "stores the size of related build artifacts" do - statistics.update_build_artifacts_size - - expect(statistics.build_artifacts_size).to be(106365) - end - - it 'calculates related build artifacts by project' do - expect(Ci::JobArtifact).to receive(:artifacts_size_for).with(project) { 0 } - - statistics.update_build_artifacts_size - end - end - - context 'when legacy artifacts are used' do - let!(:ci_build) { create(:ci_build, pipeline: pipeline, artifacts_size: 10.megabytes) } - - it "stores the size of related build artifacts" do - statistics.update_build_artifacts_size - - expect(statistics.build_artifacts_size).to eq(10.megabytes) - end - end - end - describe '#update_storage_size' do it "sums all storage counters" do statistics.update!( @@ -177,4 +120,27 @@ describe ProjectStatistics do expect(statistics.storage_size).to eq 5 end end + + describe '.increment_statistic' do + it 'increases the statistic by that amount' do + expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) } + .to change { statistics.reload.build_artifacts_size } + .by(13) + end + + context 'when the amount is 0' do + it 'does not execute a query' do + project + expect { described_class.increment_statistic(project.id, :build_artifacts_size, 0) } + .not_to exceed_query_limit(0) + end + end + + context 'when using an invalid column' do + it 'raises an error' do + expect { described_class.increment_statistic(project.id, :id, 13) } + .to raise_error(ArgumentError, "Cannot increment attribute: id") + end + end + end end diff --git a/spec/requests/api/project_snapshots_spec.rb b/spec/requests/api/project_snapshots_spec.rb new file mode 100644 index 00000000000..07a920f8d28 --- /dev/null +++ b/spec/requests/api/project_snapshots_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe API::ProjectSnapshots do + include WorkhorseHelpers + + let(:project) { create(:project) } + let(:admin) { create(:admin) } + + describe 'GET /projects/:id/snapshot' do + def expect_snapshot_response_for(repository) + type, params = workhorse_send_data + + expect(type).to eq('git-snapshot') + expect(params).to eq( + 'GitalyServer' => { + 'address' => Gitlab::GitalyClient.address(repository.project.repository_storage), + 'token' => Gitlab::GitalyClient.token(repository.project.repository_storage) + }, + 'GetSnapshotRequest' => Gitaly::GetSnapshotRequest.new( + repository: repository.gitaly_repository + ).to_json + ) + end + + it 'returns authentication error as project owner' do + get api("/projects/#{project.id}/snapshot", project.owner) + + expect(response).to have_gitlab_http_status(403) + end + + it 'returns authentication error as unauthenticated user' do + get api("/projects/#{project.id}/snapshot", nil) + + expect(response).to have_gitlab_http_status(401) + end + + it 'requests project repository raw archive as administrator' do + get api("/projects/#{project.id}/snapshot", admin), wiki: '0' + + expect(response).to have_gitlab_http_status(200) + expect_snapshot_response_for(project.repository) + end + + it 'requests wiki repository raw archive as administrator' do + get api("/projects/#{project.id}/snapshot", admin), wiki: '1' + + expect(response).to have_gitlab_http_status(200) + expect_snapshot_response_for(project.wiki.repository) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 17272cb00e5..85a571b8f0e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -685,7 +685,8 @@ describe API::Projects do issues_enabled: false, merge_requests_enabled: false, wiki_enabled: false, - request_access_enabled: true + request_access_enabled: true, + jobs_enabled: true }) post api("/projects/user/#{user.id}", admin), project diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f406d2ffb22..e8196980a8c 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -212,6 +212,18 @@ describe API::Users do expect(json_response.last['id']).to eq(user.id) end + it 'returns users with 2fa enabled' do + admin + user + user_with_2fa = create(:user, :two_factor_via_otp) + + get api('/users', admin), { two_factor: 'enabled' } + + expect(response).to match_response_schema('public_api/v4/user/admins') + expect(json_response.size).to eq(1) + expect(json_response.first['id']).to eq(user_with_2fa.id) + end + it 'returns 400 when provided incorrect sort params' do get api('/users', admin), { order_by: 'magic', sort: 'asc' } diff --git a/spec/serializers/entity_date_helper_spec.rb b/spec/serializers/entity_date_helper_spec.rb index b9cc2f64831..36da8d33a44 100644 --- a/spec/serializers/entity_date_helper_spec.rb +++ b/spec/serializers/entity_date_helper_spec.rb @@ -32,6 +32,7 @@ describe EntityDateHelper do end it 'converts 86560 seconds' do + Rails.logger.debug date_helper_class.inspect expect(date_helper_class.distance_of_time_as_hash(86560)).to eq(days: 1, mins: 2, seconds: 40) end @@ -42,4 +43,58 @@ describe EntityDateHelper do it 'converts 986760 seconds' do expect(date_helper_class.distance_of_time_as_hash(986760)).to eq(days: 11, hours: 10, mins: 6) end + + describe '#remaining_days_in_words' do + around do |example| + Timecop.freeze(Time.utc(2017, 3, 17)) { example.run } + end + + context 'when less than 31 days remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) } + + it 'returns days remaining' do + expect(milestone_remaining).to eq("<strong>12</strong> days remaining") + end + end + + context 'when less than 1 year and more than 30 days remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) } + + it 'returns months remaining' do + expect(milestone_remaining).to eq("<strong>2</strong> months remaining") + end + end + + context 'when more than 1 year remaining' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) } + + it 'returns years remaining' do + expect(milestone_remaining).to eq("<strong>1</strong> year remaining") + end + end + + context 'when milestone is expired' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, due_date: 2.days.ago.utc)) } + + it 'returns "Past due"' do + expect(milestone_remaining).to eq("<strong>Past due</strong>") + end + end + + context 'when milestone has start_date in the future' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) } + + it 'returns "Upcoming"' do + expect(milestone_remaining).to eq("<strong>Upcoming</strong>") + end + end + + context 'when milestone has start_date in the past' do + let(:milestone_remaining) { date_helper_class.remaining_days_in_words(build_stubbed(:milestone, start_date: 2.days.ago.utc)) } + + it 'returns days elapsed' do + expect(milestone_remaining).to eq("<strong>2</strong> days elapsed") + end + end + end end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index ae819c011de..80bac590a11 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -8,6 +8,7 @@ describe Labels::TransferService do let(:group_3) { create(:group) } let(:project_1) { create(:project, namespace: group_2) } let(:project_2) { create(:project, namespace: group_3) } + let(:project_3) { create(:project, namespace: group_1) } let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') } let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') } @@ -23,6 +24,7 @@ describe Labels::TransferService do create(:labeled_issue, project: project_1, labels: [group_label_4]) create(:labeled_issue, project: project_1, labels: [project_label_1]) create(:labeled_issue, project: project_2, labels: [group_label_5]) + create(:labeled_issue, project: project_3, labels: [group_label_1]) create(:labeled_merge_request, source_project: project_1, labels: [group_label_1, group_label_2]) create(:labeled_merge_request, source_project: project_2, labels: [group_label_5]) end @@ -52,5 +54,13 @@ describe Labels::TransferService do expect(project_1.labels.where(title: group_label_4.title)).to be_empty end + + it 'updates only label links in the given project' do + service.execute + + targets = LabelLink.where(label_id: group_label_1.id).map(&:target) + + expect(targets).to eq(project_3.issues) + end end end diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb index 609d678caea..d40e6f1449d 100644 --- a/spec/services/projects/create_from_template_service_spec.rb +++ b/spec/services/projects/create_from_template_service_spec.rb @@ -7,7 +7,7 @@ describe Projects::CreateFromTemplateService do path: user.to_param, template_name: 'rails', description: 'project description', - visibility_level: Gitlab::VisibilityLevel::PRIVATE + visibility_level: Gitlab::VisibilityLevel::PUBLIC } end @@ -24,7 +24,23 @@ describe Projects::CreateFromTemplateService do expect(project).to be_saved expect(project.scheduled?).to be(true) - expect(project.description).to match('project description') - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + context 'the result project' do + before do + Sidekiq::Testing.inline! do + @project = subject.execute + end + + @project.reload + end + + it 'overrides template description' do + expect(@project.description).to match('project description') + end + + it 'overrides template visibility_level' do + expect(@project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end end end diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb index 144af4fc475..6a6e13418a9 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb @@ -19,6 +19,14 @@ shared_examples_for 'AtomicInternalId' do it { is_expected.to validate_numericality_of(internal_id_attribute) } end + describe 'Creating an instance' do + subject { instance.save! } + + it 'saves a new instance properly' do + expect { subject }.not_to raise_error + end + end + describe 'internal id generation' do subject { instance.save! } diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 16455e2517b..e7277b337f6 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -75,36 +75,8 @@ describe ObjectStorage do expect(object).to receive(:file_store).and_return(nil) end - context 'when object storage is enabled' do - context 'when direct uploads are enabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) - end - - it "uses Store::REMOTE" do - is_expected.to eq(described_class::Store::REMOTE) - end - end - - context 'when direct uploads are disabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) - end - - it "uses Store::LOCAL" do - is_expected.to eq(described_class::Store::LOCAL) - end - end - end - - context 'when object storage is disabled' do - before do - stub_uploads_object_storage(uploader_class, enabled: false) - end - - it "uses Store::LOCAL" do - is_expected.to eq(described_class::Store::LOCAL) - end + it "uses Store::LOCAL" do + is_expected.to eq(described_class::Store::LOCAL) end end @@ -537,6 +509,72 @@ describe ObjectStorage do end end + context 'when local file is used' do + let(:temp_file) { Tempfile.new("test") } + + before do + FileUtils.touch(temp_file) + end + + after do + FileUtils.rm_f(temp_file) + end + + context 'when valid file is used' do + context 'when valid file is specified' do + let(:uploaded_file) { temp_file } + + context 'when object storage and direct upload is specified' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) + end + + context 'when file is stored' do + subject do + uploader.store!(uploaded_file) + end + + it 'file to be remotely stored in permament location' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader).not_to be_file_storage + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.object_store).to eq(described_class::Store::REMOTE) + end + end + end + + context 'when object storage and direct upload is not used' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) + end + + context 'when file is stored' do + subject do + uploader.store!(uploaded_file) + end + + it 'file to be remotely stored in permament location' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader).to be_file_storage + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.object_store).to eq(described_class::Store::LOCAL) + end + end + end + end + end + end + context 'when remote file is used' do let(:temp_file) { Tempfile.new("test") } @@ -590,9 +628,9 @@ describe ObjectStorage do expect(uploader).to be_exists expect(uploader).to be_cached + expect(uploader).not_to be_file_storage expect(uploader.path).not_to be_nil expect(uploader.path).not_to include('tmp/cache') - expect(uploader.url).not_to be_nil expect(uploader.path).not_to include('tmp/cache') expect(uploader.object_store).to eq(described_class::Store::REMOTE) end @@ -607,6 +645,7 @@ describe ObjectStorage do expect(uploader).to be_exists expect(uploader).not_to be_cached + expect(uploader).not_to be_file_storage expect(uploader.path).not_to be_nil expect(uploader.path).not_to include('tmp/upload') expect(uploader.path).not_to include('tmp/cache') diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb index 7b60835fd26..2710267d384 100644 --- a/spec/workers/issue_due_scheduler_worker_spec.rb +++ b/spec/workers/issue_due_scheduler_worker_spec.rb @@ -14,7 +14,9 @@ describe IssueDueSchedulerWorker do create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow) create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today) - expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]]) + expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async) do |args| + expect(args).to match_array([[project1.id], [project2.id]]) + end described_class.new.perform end |