diff options
Diffstat (limited to 'spec')
24 files changed, 954 insertions, 57 deletions
diff --git a/spec/features/issuables/close_reopen_report_toggle_spec.rb b/spec/features/issuables/close_reopen_report_toggle_spec.rb new file mode 100644 index 00000000000..9a99bb705b7 --- /dev/null +++ b/spec/features/issuables/close_reopen_report_toggle_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe 'Issuables Close/Reopen/Report toggle', :feature do + let(:user) { create(:user) } + + shared_examples 'an issuable close/reopen/report toggle' do + let(:container) { find('.issuable-close-dropdown') } + let(:human_model_name) { issuable.model_name.human.downcase } + + it 'shows toggle' do + expect(page).to have_link("Close #{human_model_name}") + expect(page).to have_selector('.issuable-close-dropdown') + end + + it 'opens a dropdown when toggle is clicked' do + container.find('.dropdown-toggle').click + + expect(container).to have_selector('.dropdown-menu') + expect(container).to have_content("Close #{human_model_name}") + expect(container).to have_content('Report abuse') + expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.") + expect(container).to have_selector('.close-item.droplab-item-selected') + expect(container).to have_selector('.report-item') + expect(container).not_to have_selector('.report-item.droplab-item-selected') + expect(container).not_to have_selector('.reopen-item') + end + + it 'changes the button when an item is selected' do + button = container.find('.issuable-close-button') + + container.find('.dropdown-toggle').click + container.find('.report-item').click + + expect(container).not_to have_selector('.dropdown-menu') + expect(button).to have_content('Report abuse') + + container.find('.dropdown-toggle').click + container.find('.close-item').click + + expect(button).to have_content("Close #{human_model_name}") + end + end + + context 'on an issue' do + let(:project) { create(:empty_project) } + let(:issuable) { create(:issue, project: project) } + + before do + project.add_master(user) + login_as user + end + + context 'when user has permission to update', :js do + before do + visit project_issue_path(project, issuable) + end + + it_behaves_like 'an issuable close/reopen/report toggle' + end + + context 'when user doesnt have permission to update' do + let(:cant_project) { create(:empty_project) } + let(:cant_issuable) { create(:issue, project: cant_project) } + + before do + cant_project.add_guest(user) + + visit project_issue_path(cant_project, cant_issuable) + end + + it 'only shows the `Report abuse` and `New issue` buttons' do + expect(page).to have_link('Report abuse') + expect(page).to have_link('New issue') + expect(page).not_to have_link('Close issue') + expect(page).not_to have_link('Reopen issue') + expect(page).not_to have_link('Edit') + end + end + end + + context 'on a merge request' do + let(:project) { create(:project) } + let(:issuable) { create(:merge_request, source_project: project) } + + before do + project.add_master(user) + login_as user + end + + context 'when user has permission to update', :js do + before do + visit project_merge_request_path(project, issuable) + end + + it_behaves_like 'an issuable close/reopen/report toggle' + end + + context 'when user doesnt have permission to update' do + let(:cant_project) { create(:project) } + let(:cant_issuable) { create(:merge_request, source_project: cant_project) } + + before do + cant_project.add_reporter(user) + + visit project_merge_request_path(cant_project, cant_issuable) + end + + it 'only shows a `Report abuse` button' do + expect(page).to have_link('Report abuse') + expect(page).not_to have_link('Close merge request') + expect(page).not_to have_link('Reopen merge request') + expect(page).not_to have_link('Edit') + end + end + end +end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 0412c186a90..a15c3d1d447 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -133,7 +133,7 @@ describe 'Visual tokens', js: true, feature: true do describe 'editing milestone token' do before do input_filtered_search('milestone:%10.0 author:none', submit: false) - first('.tokens-container .filtered-search-token').double_click + first('.tokens-container .filtered-search-token').click first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item') end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 2193c5fd69c..9b4cc653af5 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -14,6 +14,18 @@ feature 'GFM autocomplete', feature: true, js: true do wait_for_requests end + it 'updates issue descripton with GFM reference' do + find('.issuable-edit').click + + find('#issue-description').native.send_keys("@#{user.name[0...3]}") + + find('.atwho-view .cur').trigger('click') + + click_button 'Save changes' + + expect(find('.description')).to have_content(user.to_reference) + end + it 'opens autocomplete menu when field starts with text' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb index 1b6d1f3415f..42764e808e6 100644 --- a/spec/features/oauth_login_spec.rb +++ b/spec/features/oauth_login_spec.rb @@ -13,7 +13,7 @@ feature 'OAuth Login', js: true do end providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, - :facebook, :cas3, :auth0] + :facebook, :cas3, :auth0, :authentiq] before(:all) do # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 8464bf50cac..670e8dda916 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do end end + let(:group) { create(:group) } + context 'when user is logged-out' do before do visit root_path end - context 'when the gitlab_performance_bar feature is disabled' do + context 'when the performance_bar feature is disabled' do before do - Feature.disable('gitlab_performance_bar') + stub_application_setting(performance_bar_allowed_group_id: nil) end it_behaves_like 'performance bar is disabled' end - context 'when the gitlab_performance_bar feature is enabled' do + context 'when the performance_bar feature is enabled' do before do - Feature.enable('gitlab_performance_bar') + stub_application_setting(performance_bar_allowed_group_id: group.id) end it_behaves_like 'performance bar is disabled' @@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do context 'when user is logged-in' do before do - sign_in(create(:user)) + user = create(:user) + + sign_in(user) + group.add_guest(user) visit root_path end - context 'when the gitlab_performance_bar feature is disabled' do + context 'when the performance_bar feature is disabled' do before do - Feature.disable('gitlab_performance_bar') + stub_application_setting(performance_bar_allowed_group_id: nil) end it_behaves_like 'performance bar is disabled' end - context 'when the gitlab_performance_bar feature is enabled' do + context 'when the performance_bar feature is enabled' do before do - Feature.enable('gitlab_performance_bar') + stub_application_setting(performance_bar_allowed_group_id: group.id) end it_behaves_like 'performance bar is enabled' diff --git a/spec/javascripts/close_reopen_report_toggle_spec.js b/spec/javascripts/close_reopen_report_toggle_spec.js new file mode 100644 index 00000000000..925e959c85a --- /dev/null +++ b/spec/javascripts/close_reopen_report_toggle_spec.js @@ -0,0 +1,270 @@ +import CloseReopenReportToggle from '~/close_reopen_report_toggle'; +import DropLab from '~/droplab/drop_lab'; + +describe('CloseReopenReportToggle', () => { + describe('class constructor', () => { + const dropdownTrigger = {}; + const dropdownList = {}; + const button = {}; + let commentTypeToggle; + + beforeEach(function () { + commentTypeToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + }); + + it('sets .dropdownTrigger', function () { + expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger); + }); + + it('sets .dropdownList', function () { + expect(commentTypeToggle.dropdownList).toBe(dropdownList); + }); + + it('sets .button', function () { + expect(commentTypeToggle.button).toBe(button); + }); + }); + + describe('initDroplab', () => { + let closeReopenReportToggle; + const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']); + const dropdownTrigger = {}; + const button = {}; + const reopenItem = {}; + const closeItem = {}; + const config = {}; + + beforeEach(() => { + spyOn(DropLab.prototype, 'init'); + dropdownList.querySelector.and.returnValues(reopenItem, closeItem); + + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config); + + closeReopenReportToggle.initDroplab(); + }); + + it('sets .reopenItem and .closeItem', () => { + expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item'); + expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item'); + expect(closeReopenReportToggle.reopenItem).toBe(reopenItem); + expect(closeReopenReportToggle.closeItem).toBe(closeItem); + }); + + it('sets .droplab', () => { + expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object)); + }); + + it('calls .setConfig', () => { + expect(closeReopenReportToggle.setConfig).toHaveBeenCalled(); + }); + + it('calls droplab.init', () => { + expect(DropLab.prototype.init).toHaveBeenCalledWith( + dropdownTrigger, + dropdownList, + jasmine.any(Array), + config, + ); + }); + }); + + describe('updateButton', () => { + let closeReopenReportToggle; + const dropdownList = {}; + const dropdownTrigger = {}; + const button = jasmine.createSpyObj('button', ['blur']); + const isClosed = true; + + beforeEach(() => { + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + spyOn(closeReopenReportToggle, 'toggleButtonType'); + + closeReopenReportToggle.updateButton(isClosed); + }); + + it('calls .toggleButtonType', () => { + expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed); + }); + + it('calls .button.blur', () => { + expect(closeReopenReportToggle.button.blur).toHaveBeenCalled(); + }); + }); + + describe('toggleButtonType', () => { + let closeReopenReportToggle; + const dropdownList = {}; + const dropdownTrigger = {}; + const button = {}; + const isClosed = true; + const showItem = jasmine.createSpyObj('showItem', ['click']); + const hideItem = {}; + showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']); + hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']); + + beforeEach(() => { + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]); + + closeReopenReportToggle.toggleButtonType(isClosed); + }); + + it('calls .getButtonTypes', () => { + expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed); + }); + + it('removes hide class and add selected class to showItem, opposite for hideItem', () => { + expect(showItem.classList.remove).toHaveBeenCalledWith('hidden'); + expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected'); + expect(hideItem.classList.add).toHaveBeenCalledWith('hidden'); + expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected'); + }); + + it('clicks the showItem', () => { + expect(showItem.click).toHaveBeenCalled(); + }); + }); + + describe('getButtonTypes', () => { + let closeReopenReportToggle; + const dropdownList = {}; + const dropdownTrigger = {}; + const button = {}; + const reopenItem = {}; + const closeItem = {}; + + beforeEach(() => { + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + closeReopenReportToggle.reopenItem = reopenItem; + closeReopenReportToggle.closeItem = closeItem; + }); + + it('returns reopenItem, closeItem if isClosed is true', () => { + const buttonTypes = closeReopenReportToggle.getButtonTypes(true); + + expect(buttonTypes).toEqual([reopenItem, closeItem]); + }); + + it('returns closeItem, reopenItem if isClosed is false', () => { + const buttonTypes = closeReopenReportToggle.getButtonTypes(false); + + expect(buttonTypes).toEqual([closeItem, reopenItem]); + }); + }); + + describe('setDisable', () => { + let closeReopenReportToggle; + const dropdownList = {}; + const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']); + const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']); + + beforeEach(() => { + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + }); + + it('disable .button and .dropdownTrigger if shouldDisable is true', () => { + closeReopenReportToggle.setDisable(true); + + expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true'); + expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true'); + }); + + it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => { + closeReopenReportToggle.setDisable(); + + expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true'); + expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true'); + }); + + it('enable .button and .dropdownTrigger if shouldDisable is false', () => { + closeReopenReportToggle.setDisable(false); + + expect(button.removeAttribute).toHaveBeenCalledWith('disabled'); + expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled'); + }); + }); + + describe('setConfig', () => { + let closeReopenReportToggle; + const dropdownList = {}; + const dropdownTrigger = {}; + const button = {}; + let config; + + beforeEach(() => { + closeReopenReportToggle = new CloseReopenReportToggle({ + dropdownTrigger, + dropdownList, + button, + }); + + config = closeReopenReportToggle.setConfig(); + }); + + it('returns a config object', () => { + expect(config).toEqual({ + InputSetter: [ + { + input: button, + valueAttribute: 'data-text', + inputAttribute: 'data-value', + }, + { + input: button, + valueAttribute: 'data-text', + inputAttribute: 'title', + }, + { + input: button, + valueAttribute: 'data-button-class', + inputAttribute: 'class', + }, + { + input: dropdownTrigger, + valueAttribute: 'data-toggle-class', + inputAttribute: 'class', + }, + { + input: button, + valueAttribute: 'data-url', + inputAttribute: 'href', + }, + { + input: button, + valueAttribute: 'data-method', + inputAttribute: 'data-method', + }, + ], + }); + }); + }); +}); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index df97a100b0d..0c8c4d2cea6 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,10 +1,10 @@ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ import Issue from '~/issue'; - +import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import '~/lib/utils/text_utility'; describe('Issue', function() { - let $boxClosed, $boxOpen, $btnClose, $btnReopen; + let $boxClosed, $boxOpen, $btn; preloadFixtures('issues/closed-issue.html.raw'); preloadFixtures('issues/issue-with-task-list.html.raw'); @@ -20,9 +20,7 @@ describe('Issue', function() { function expectIssueState(isIssueOpen) { expectVisibility($boxClosed, !isIssueOpen); expectVisibility($boxOpen, isIssueOpen); - - expectVisibility($btnClose, isIssueOpen); - expectVisibility($btnReopen, !isIssueOpen); + expect($btn).toHaveText(isIssueOpen ? 'Close issue' : 'Reopen issue'); } function expectNewBranchButtonState(isPending, canCreate) { @@ -57,7 +55,7 @@ describe('Issue', function() { } } - function findElements() { + function findElements(isIssueInitiallyOpen) { $boxClosed = $('div.status-box-closed'); expect($boxClosed).toExist(); expect($boxClosed).toHaveText('Closed'); @@ -66,13 +64,9 @@ describe('Issue', function() { expect($boxOpen).toExist(); expect($boxOpen).toHaveText('Open'); - $btnClose = $('.btn-close.btn-grouped'); - expect($btnClose).toExist(); - expect($btnClose).toHaveText('Close issue'); - - $btnReopen = $('.btn-reopen.btn-grouped'); - expect($btnReopen).toExist(); - expect($btnReopen).toHaveText('Reopen issue'); + $btn = $('.js-issuable-close-button'); + expect($btn).toExist(); + expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue'); } describe('task lists', function() { @@ -99,7 +93,6 @@ describe('Issue', function() { function ajaxSpy(req) { if (req.url === this.$triggeredButton.attr('href')) { expect(req.type).toBe('PUT'); - expect(this.$triggeredButton).toHaveProp('disabled', true); expectNewBranchButtonState(true, false); return this.issueStateDeferred; } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) { @@ -119,10 +112,11 @@ describe('Issue', function() { loadFixtures('issues/closed-issue.html.raw'); } - findElements(); + findElements(isIssueInitiallyOpen); this.issue = new Issue(); expectIssueState(isIssueInitiallyOpen); - this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen; + + this.$triggeredButton = $btn; this.$projectIssuesCounter = $('.issue_counter'); this.$projectIssuesCounter.text('1,001'); @@ -143,7 +137,7 @@ describe('Issue', function() { }); expectIssueState(!isIssueInitiallyOpen); - expect(this.$triggeredButton).toHaveProp('disabled', false); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); expectNewBranchButtonState(false, !isIssueInitiallyOpen); }); @@ -158,7 +152,7 @@ describe('Issue', function() { }); expectIssueState(isIssueInitiallyOpen); - expect(this.$triggeredButton).toHaveProp('disabled', false); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); expectErrorMessage(); expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectNewBranchButtonState(false, isIssueInitiallyOpen); @@ -172,7 +166,7 @@ describe('Issue', function() { }); expectIssueState(isIssueInitiallyOpen); - expect(this.$triggeredButton).toHaveProp('disabled', true); + expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); expectErrorMessage(); expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectNewBranchButtonState(false, isIssueInitiallyOpen); @@ -195,4 +189,37 @@ describe('Issue', function() { }); }); }); + + describe('units', () => { + describe('class constructor', () => { + it('calls .initCloseReopenReport', () => { + spyOn(Issue.prototype, 'initCloseReopenReport'); + + new Issue(); // eslint-disable-line no-new + + expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled(); + }); + }); + + describe('initCloseReopenReport', () => { + it('calls .initDroplab', () => { + const container = jasmine.createSpyObj('container', ['querySelector']); + const dropdownTrigger = {}; + const dropdownList = {}; + const button = {}; + + spyOn(document, 'querySelector').and.returnValue(container); + spyOn(CloseReopenReportToggle.prototype, 'initDroplab'); + container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button); + + Issue.prototype.initCloseReopenReport(); + + expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button'); + expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); + }); + }); + }); }); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index f444bcaf847..6ff42e2378d 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -2,10 +2,12 @@ /* global MergeRequest */ import '~/merge_request'; +import CloseReopenReportToggle from '~/close_reopen_report_toggle'; +import IssuablesHelper from '~/helpers/issuables_helper'; (function() { describe('MergeRequest', function() { - return describe('task lists', function() { + describe('task lists', function() { preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); beforeEach(function() { loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); @@ -27,5 +29,34 @@ import '~/merge_request'; return $('.js-task-list-field').trigger('tasklist:changed'); }); }); + + describe('class constructor', () => { + it('calls .initCloseReopenReport', () => { + spyOn(IssuablesHelper, 'initCloseReopenReport'); + + new MergeRequest(); // eslint-disable-line no-new + + expect(IssuablesHelper.initCloseReopenReport).toHaveBeenCalled(); + }); + + it('calls .initDroplab', () => { + const container = jasmine.createSpyObj('container', ['querySelector']); + const dropdownTrigger = {}; + const dropdownList = {}; + const button = {}; + + spyOn(CloseReopenReportToggle.prototype, 'initDroplab'); + spyOn(document, 'querySelector').and.returnValue(container); + container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button); + + new MergeRequest(); // eslint-disable-line no-new + + expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu'); + expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button'); + expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled(); + }); + }); }); }).call(window); diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb index f2132d485ab..dfffef8b9b7 100644 --- a/spec/lib/extracts_path_spec.rb +++ b/spec/lib/extracts_path_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ExtractsPath, lib: true do include ExtractsPath include RepoHelpers - include Gitlab::Routing.url_helpers + include Gitlab::Routing let(:project) { double('project') } let(:request) { double('request') } diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 58d3ee6b488..3c784eda4f8 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -111,7 +111,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe '.raw' do + shared_examples 'finding blobs by ID' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } @@ -136,6 +136,16 @@ describe Gitlab::Git::Blob, seed_helper: true do end end + describe '.raw' do + context 'when the blob_raw Gitaly feature is enabled' do + it_behaves_like 'finding blobs by ID' + end + + context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding blobs by ID' + end + end + describe 'encoding' do context 'file with russian text' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb new file mode 100644 index 00000000000..8a586bdbf63 --- /dev/null +++ b/spec/lib/gitlab/performance_bar_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe Gitlab::PerformanceBar do + shared_examples 'allowed user IDs are cached' do + before do + # Warm the Redis cache + described_class.enabled?(user) + end + + it 'caches the allowed user IDs in cache', :caching do + expect do + expect(described_class.enabled?(user)).to be_truthy + end.not_to exceed_query_limit(0) + end + end + + describe '.enabled?' do + let(:user) { create(:user) } + + before do + stub_application_setting(performance_bar_allowed_group_id: -1) + end + + it 'returns false when given user is nil' do + expect(described_class.enabled?(nil)).to be_falsy + end + + it 'returns false when allowed_group_id is nil' do + expect(described_class).to receive(:allowed_group_id).and_return(nil) + + expect(described_class.enabled?(user)).to be_falsy + end + + context 'when allowed group ID does not exist' do + it 'returns false' do + expect(described_class.enabled?(user)).to be_falsy + end + end + + context 'when allowed group exists' do + let!(:my_group) { create(:group, path: 'my-group') } + + before do + stub_application_setting(performance_bar_allowed_group_id: my_group.id) + end + + context 'when user is not a member of the allowed group' do + it 'returns false' do + expect(described_class.enabled?(user)).to be_falsy + end + + it_behaves_like 'allowed user IDs are cached' + end + + context 'when user is a member of the allowed group' do + before do + my_group.add_developer(user) + end + + it 'returns true' do + expect(described_class.enabled?(user)).to be_truthy + end + + it_behaves_like 'allowed user IDs are cached' + end + end + + context 'when allowed group is nested', :nested_groups do + let!(:nested_my_group) { create(:group, parent: create(:group, path: 'my-org'), path: 'my-group') } + + before do + create(:group, path: 'my-group') + nested_my_group.add_developer(user) + stub_application_setting(performance_bar_allowed_group_id: nested_my_group.id) + end + + it 'returns the nested group' do + expect(described_class.enabled?(user)).to be_truthy + end + end + + context 'when a nested group has the same path', :nested_groups do + before do + create(:group, :nested, path: 'my-group').add_developer(user) + end + + it 'returns false' do + expect(described_class.enabled?(user)).to be_falsy + end + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 166a4474abf..fb485d0b2c6 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -214,6 +214,160 @@ describe ApplicationSetting, models: true do end end + describe 'performance bar settings' do + describe 'performance_bar_allowed_group_id=' do + context 'with a blank path' do + before do + setting.performance_bar_allowed_group_id = create(:group).full_path + end + + it 'persists nil for a "" path and clears allowed user IDs cache' do + expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_allowed_group_id = '' + + expect(setting.performance_bar_allowed_group_id).to be_nil + end + end + + context 'with an invalid path' do + it 'does not persist an invalid group path' do + setting.performance_bar_allowed_group_id = 'foo' + + expect(setting.performance_bar_allowed_group_id).to be_nil + end + end + + context 'with a path to an existing group' do + let(:group) { create(:group) } + + it 'persists a valid group path and clears allowed user IDs cache' do + expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_allowed_group_id = group.full_path + + expect(setting.performance_bar_allowed_group_id).to eq(group.id) + end + + context 'when the given path is the same' do + context 'with a blank path' do + before do + setting.performance_bar_allowed_group_id = nil + end + + it 'clears the cached allowed user IDs' do + expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_allowed_group_id = '' + end + end + + context 'with a valid path' do + before do + setting.performance_bar_allowed_group_id = group.full_path + end + + it 'clears the cached allowed user IDs' do + expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_allowed_group_id = group.full_path + end + end + end + end + end + + describe 'performance_bar_allowed_group' do + context 'with no performance_bar_allowed_group_id saved' do + it 'returns nil' do + expect(setting.performance_bar_allowed_group).to be_nil + end + end + + context 'with a performance_bar_allowed_group_id saved' do + let(:group) { create(:group) } + + before do + setting.performance_bar_allowed_group_id = group.full_path + end + + it 'returns the group' do + expect(setting.performance_bar_allowed_group).to eq(group) + end + end + end + + describe 'performance_bar_enabled' do + context 'with the Performance Bar is enabled' do + let(:group) { create(:group) } + + before do + setting.performance_bar_allowed_group_id = group.full_path + end + + it 'returns true' do + expect(setting.performance_bar_enabled).to be_truthy + end + end + end + + describe 'performance_bar_enabled=' do + context 'when the performance bar is enabled' do + let(:group) { create(:group) } + + before do + setting.performance_bar_allowed_group_id = group.full_path + end + + context 'when passing true' do + it 'does not clear allowed user IDs cache' do + expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_enabled = true + + expect(setting.performance_bar_allowed_group_id).to eq(group.id) + expect(setting.performance_bar_enabled).to be_truthy + end + end + + context 'when passing false' do + it 'disables the performance bar and clears allowed user IDs cache' do + expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_enabled = false + + expect(setting.performance_bar_allowed_group_id).to be_nil + expect(setting.performance_bar_enabled).to be_falsey + end + end + end + + context 'when the performance bar is disabled' do + context 'when passing true' do + it 'does nothing and does not clear allowed user IDs cache' do + expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_enabled = true + + expect(setting.performance_bar_allowed_group_id).to be_nil + expect(setting.performance_bar_enabled).to be_falsey + end + end + + context 'when passing false' do + it 'does nothing and does not clear allowed user IDs cache' do + expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache) + + setting.performance_bar_enabled = false + + expect(setting.performance_bar_allowed_group_id).to be_nil + expect(setting.performance_bar_enabled).to be_falsey + end + end + end + end + end + describe 'usage ping settings' do context 'when the usage ping is disabled in gitlab.yml' do before do diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb new file mode 100644 index 00000000000..951690a217b --- /dev/null +++ b/spec/models/concerns/each_batch_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe EachBatch do + describe '.each_batch' do + let(:model) do + Class.new(ActiveRecord::Base) do + include EachBatch + + self.table_name = 'users' + end + end + + before do + 5.times { create(:user, updated_at: 1.day.ago) } + end + + it 'yields an ActiveRecord::Relation when a block is given' do + model.each_batch do |relation| + expect(relation).to be_a_kind_of(ActiveRecord::Relation) + end + end + + it 'yields a batch index as the second argument' do + model.each_batch do |_, index| + expect(index).to eq(1) + end + end + + it 'accepts a custom batch size' do + amount = 0 + + model.each_batch(of: 1) { amount += 1 } + + expect(amount).to eq(5) + end + + it 'does not include ORDER BYs in the yielded relations' do + model.each_batch do |relation| + expect(relation.to_sql).not_to include('ORDER BY') + end + end + + it 'allows updating of the yielded relations' do + time = Time.now + + model.each_batch do |relation| + relation.update_all(updated_at: time) + end + + expect(model.where(updated_at: time).count).to eq(5) + end + end +end diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb index 610793ee557..21893e0cbaa 100644 --- a/spec/models/concerns/sha_attribute_spec.rb +++ b/spec/models/concerns/sha_attribute_spec.rb @@ -13,7 +13,7 @@ describe ShaAttribute do end describe '#sha_attribute' do - context' when the table exists' do + context 'when the table exists' do before do allow(model).to receive(:table_exists?).and_return(true) end @@ -30,7 +30,7 @@ describe ShaAttribute do end end - context' when the table does not exist' do + context 'when the table does not exist' do before do allow(model).to receive(:table_exists?).and_return(false) end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 399020953e8..066d7b9307f 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -466,13 +466,13 @@ describe Group, models: true do it_behaves_like 'ref is protected' end - context 'when group has children' do - let!(:group_child) { create(:group, parent: group) } - let!(:variable_child) { create(:ci_group_variable, group: group_child) } - let!(:group_child_3) { create(:group, parent: group_child_2) } - let!(:variable_child_3) { create(:ci_group_variable, group: group_child_3) } - let!(:group_child_2) { create(:group, parent: group_child) } - let!(:variable_child_2) { create(:ci_group_variable, group: group_child_2) } + context 'when group has children', :postgresql do + let(:group_child) { create(:group, parent: group) } + let(:group_child_2) { create(:group, parent: group_child) } + let(:group_child_3) { create(:group, parent: group_child_2) } + let(:variable_child) { create(:ci_group_variable, group: group_child) } + let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) } + let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) } it 'returns all variables belong to the group and parent groups' do expected_array1 = [protected_variable, secret_variable] diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 4a1de76f099..105afed1337 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe JiraService, models: true do - include Gitlab::Routing.url_helpers + include Gitlab::Routing describe "Associations" do it { is_expected.to belong_to :project } diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 1d8aaeea8f2..7e21006b254 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -113,6 +113,20 @@ describe API::Features do { 'key' => 'actors', 'value' => ["User:#{user.id}"] } ]) end + + it 'creates an enabled feature for the given user and feature group when passed user=username and feature_group=perf_team' do + post api("/features/#{feature_name}", admin), value: 'true', user: user.username, feature_group: 'perf_team' + + expect(response).to have_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'groups', 'value' => ['perf_team'] }, + { 'key' => 'actors', 'value' => ["User:#{user.id}"] } + ]) + end end it 'creates a feature with the given percentage if passed an integer' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 9b53164b4a2..9837fedb522 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1462,6 +1462,25 @@ describe API::Issues do end end + describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do + let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) } + + it 'exposes known attributes' do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin) + + expect(response).to have_http_status(200) + expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) + expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) + expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) + end + + it "returns unautorized for non-admin users" do + get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user) + + expect(response).to have_http_status(403) + end + end + def expect_paginated_array_response(size: nil) expect(response).to have_http_status(200) expect(response).to include_pagination_headers diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 518639f45a2..f220972bae3 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -5,6 +5,26 @@ describe API::ProjectSnippets do let(:user) { create(:user) } let(:admin) { create(:admin) } + describe "GET /projects/:project_id/snippets/:id/user_agent_detail" do + let(:snippet) { create(:project_snippet, :public, project: project) } + let!(:user_agent_detail) { create(:user_agent_detail, subject: snippet) } + + it 'exposes known attributes' do + get api("/projects/#{project.id}/snippets/#{snippet.id}/user_agent_detail", admin) + + expect(response).to have_http_status(200) + expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) + expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) + expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) + end + + it "returns unautorized for non-admin users" do + get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/user_agent_detail", user) + + expect(response).to have_http_status(403) + end + end + describe 'GET /projects/:project_id/snippets/' do let(:user) { create(:user) } @@ -20,7 +40,7 @@ describe API::ProjectSnippets do expect(response).to include_pagination_headers expect(json_response).to be_an Array expect(json_response.size).to eq(3) - expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id) + expect(json_response.map { |snippet| snippet['id'] }).to include(public_snippet.id, internal_snippet.id, private_snippet.id) expect(json_response.last).to have_key('web_url') end @@ -38,7 +58,7 @@ describe API::ProjectSnippets do describe 'GET /projects/:project_id/snippets/:id' do let(:user) { create(:user) } - let(:snippet) { create(:project_snippet, :public, project: project) } + let(:snippet) { create(:project_snippet, :public, project: project) } it 'returns snippet json' do get api("/projects/#{project.id}/snippets/#{snippet.id}", user) diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index b20a187acfe..373fab4d98a 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -271,4 +271,25 @@ describe API::Snippets do expect(json_response['message']).to eq('404 Snippet Not Found') end end + + describe "GET /snippets/:id/user_agent_detail" do + let(:admin) { create(:admin) } + let(:snippet) { create(:personal_snippet, :public, author: user) } + let!(:user_agent_detail) { create(:user_agent_detail, subject: snippet) } + + it 'exposes known attributes' do + get api("/snippets/#{snippet.id}/user_agent_detail", admin) + + expect(response).to have_http_status(200) + expect(json_response['user_agent']).to eq(user_agent_detail.user_agent) + expect(json_response['ip_address']).to eq(user_agent_detail.ip_address) + expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted) + end + + it "returns unautorized for non-admin users" do + get api("/snippets/#{snippet.id}/user_agent_detail", user) + + expect(response).to have_http_status(403) + end + end end diff --git a/spec/rubocop/cop/in_batches_spec.rb b/spec/rubocop/cop/in_batches_spec.rb new file mode 100644 index 00000000000..072481984c6 --- /dev/null +++ b/spec/rubocop/cop/in_batches_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/in_batches' + +describe RuboCop::Cop::InBatches do + include CopHelper + + subject(:cop) { described_class.new } + + it 'registers an offense when in_batches is used' do + inspect_source(cop, 'foo.in_batches do; end') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 671a932441e..74dcf152cb8 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -98,18 +98,52 @@ describe MergeRequests::RefreshService, services: true do end context 'push to origin repo target branch' do - before do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') - reload_mrs + context 'when all MRs to the target branch had diffs' do + before do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end - it 'updates the merge state' do - expect(@merge_request.notes.last.note).to include('merged') - expect(@merge_request).to be_merged - expect(@fork_merge_request).to be_merged - expect(@fork_merge_request.notes.last.note).to include('merged') - expect(@build_failed_todo).to be_done - expect(@fork_build_failed_todo).to be_done + context 'when an MR to be closed was empty already' do + let!(:empty_fork_merge_request) do + create(:merge_request, + source_project: @fork_project, + source_branch: 'master', + target_branch: 'master', + target_project: @project) + end + + before do + # This spec already has a fake push, so pretend that we were targeting + # feature all along. + empty_fork_merge_request.update_columns(target_branch: 'feature') + + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + empty_fork_merge_request.reload + end + + it 'only updates the non-empty MRs' do + expect(@merge_request).to be_merged + expect(@merge_request.notes.last.note).to include('merged') + + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + + expect(empty_fork_merge_request).to be_open + expect(empty_fork_merge_request.merge_request_diff.state).to eq('empty') + expect(empty_fork_merge_request.notes).to be_empty + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index e35e4c1d631..60477b8e9ba 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe SystemNoteService, services: true do - include Gitlab::Routing.url_helpers + include Gitlab::Routing let(:project) { create(:empty_project) } let(:author) { create(:user) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3e90a642d56..a497b8613bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -57,7 +57,7 @@ RSpec.configure do |config| config.include StubGitlabCalls config.include StubGitlabData config.include ApiHelpers, :api - config.include Gitlab::Routing.url_helpers, type: :routing + config.include Gitlab::Routing, type: :routing config.include MigrationsHelpers, :migration config.infer_spec_type_from_file_location! |