diff options
Diffstat (limited to 'spec')
53 files changed, 1230 insertions, 690 deletions
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index d7825364ed5..c1f42bbb9d7 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -8,6 +8,10 @@ describe IssuableCollections do def self.helper_method(name); end include IssuableCollections + + def finder_type + IssuesFinder + end end controller = klass.new diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index b02d2d4261c..cc12a1005ba 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -66,15 +66,16 @@ feature 'Milestone' do end end - feature 'Open a milestone' do + feature 'Open a milestone', :js do scenario 'shows total issue time spent correctly when no time has been logged' do milestone = create(:milestone, project: project, title: 8.7) visit project_milestone_path(project, milestone) - page.within('.block.time_spent') do - expect(page).to have_content 'No time spent' - expect(page).to have_content 'None' + wait_for_requests + + page.within('.time-tracking-no-tracking-pane') do + expect(page).to have_content 'No estimate or time spent' end end @@ -89,8 +90,10 @@ feature 'Milestone' do visit project_milestone_path(project, milestone) - page.within('.block.time_spent') do - expect(page).to have_content '3h' + wait_for_requests + + page.within('.time-tracking-spend-only-pane') do + expect(page).to have_content 'Spent: 3h' end end end diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 4cf48098401..134c8b8bc39 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -149,6 +149,11 @@ feature 'Project > Members > Share with Group', :js do create(:group).add_owner(master) visit project_settings_members_path(project) + + click_link 'Share with group' + + find('.ajax-groups-select.select2-container') + execute_script 'GROUP_SELECT_PER_PAGE = 1;' open_select2 '#link_group_id' end diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb new file mode 100644 index 00000000000..9f9a7787093 --- /dev/null +++ b/spec/features/projects/network_graph_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe 'Project Network Graph', :js do + let(:user) { create :user } + let(:project) { create :project, :repository, namespace: user.namespace } + + before do + sign_in(user) + + # Stub Graph max_size to speed up test (10 commits vs. 650) + allow(Network::Graph).to receive(:max_count).and_return(10) + end + + context 'when branch is master' do + def switch_ref_to(ref_name) + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link ref_name + end + end + + def click_show_only_selected_branch_checkbox + find('#filter_ref').click + end + + before do + visit project_network_path(project, 'master') + end + + it 'renders project network' do + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'master' + end + end + + it 'switches ref to branch' do + switch_ref_to('feature') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' + end + end + + it 'switches ref to tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'renders by commit sha of "v1.0.0"' do + page.within ".network-form" do + fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + find('button').click + end + + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'filters select tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + end + + it 'renders error message when sha commit not exists' do + page.within ".network-form" do + fill_in 'extended_sha1', with: ';' + find('button').click + end + + expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist." + end + end + + it 'renders project network with test branch' do + visit project_network_path(project, "'test'") + + page.within '.network-graph' do + expect(page).to have_content "'test'" + end + end +end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 2e334caa98f..3f1ef0b2a47 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -17,6 +17,7 @@ feature 'Pages' do scenario 'does not see anything to destroy' do visit project_pages_path(project) + expect(page).to have_content('Configure pages') expect(page).not_to have_link('Remove pages') expect(page).not_to have_text('Only the project owner can remove pages') end @@ -32,14 +33,163 @@ feature 'Pages' do allow_any_instance_of(Project).to receive(:pages_deployed?) { true } end - scenario 'sees "Remove pages" link' do + scenario 'renders Access pages' do visit project_pages_path(project) - expect(page).to have_link('Remove pages') + expect(page).to have_content('Access pages') + end + + context 'when support for external domains is disabled' do + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + it 'renders message that support is disabled' do + visit project_pages_path(project) + + expect(page).to have_content('Support for domains and certificates is disabled') + end + end + + context 'when pages are exposed on external HTTP address' do + shared_examples 'adds new domain' do + it 'adds new domain' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end + end + + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + it 'allows to add new domain' do + visit project_pages_path(project) + + expect(page).to have_content('New Domain') + end + + it_behaves_like 'adds new domain' + + context 'when project in group namespace' do + it_behaves_like 'adds new domain' do + let(:group) { create :group } + let(:project) { create :project, namespace: group } + end + end + + context 'when pages domain is added' do + before do + project.pages_domains.create!(domain: 'my.test.domain.com') + + visit new_project_pages_domain_path(project) + end + + it 'renders certificates is disabled' do + expect(page).to have_content('Support for custom certificates is disabled') + end + + it 'does not adds new domain and renders error message' do + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domain has already been taken') + end + end + end + + context 'when pages are exposed on external HTTPS address' do + let(:certificate_pem) do + <<~PEM + -----BEGIN CERTIFICATE----- + MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 + LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ + MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw + gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa + SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT + nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w + DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD + VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh + IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ + joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese + 5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg + YHi2yesCrOvVXt+lgPTd + -----END CERTIFICATE----- + PEM + end + + let(:certificate_key) do + <<~KEY + -----BEGIN PRIVATE KEY----- + MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN + SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t + PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB + kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd + j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ + uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR + 5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O + AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K + EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh + Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C + m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH + EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx + 63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi + nNp/xedE1YxutQ== + -----END PRIVATE KEY----- + KEY + end + + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443']) + end + + it 'adds new domain with certificate' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + fill_in 'Certificate (PEM)', with: certificate_pem + fill_in 'Key (PEM)', with: certificate_key + click_button 'Create New Domain' + + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end end end it_behaves_like 'no pages deployed' + + describe 'project settings page' do + it 'renders "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).to have_link('Pages') + end + end + + context 'when pages are disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + it 'does not render "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).not_to have_link('Pages') + end + end + end + end end context 'when the user is not the owner' do @@ -57,4 +207,54 @@ feature 'Pages' do it_behaves_like 'no pages deployed' end + + describe 'Remove page' do + context 'when user is the owner' do + let(:project) { create :project, :repository } + + before do + project.namespace.update(owner: user) + end + + context 'when pages are deployed' do + let(:pipeline) do + commit_sha = project.commit('HEAD').sha + + project.pipelines.create( + ref: 'HEAD', + sha: commit_sha, + source: :push, + protected: false + ) + end + + let(:ci_build) do + build( + :ci_build, + project: project, + pipeline: pipeline, + ref: 'HEAD', + legacy_artifacts_file: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip')), + legacy_artifacts_metadata: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip.meta')) + ) + end + + before do + result = Projects::UpdatePagesService.new(project, ci_build).execute + expect(result[:status]).to eq(:success) + expect(project).to be_pages_deployed + end + + it 'removes the pages' do + visit project_pages_path(project) + + expect(page).to have_link('Remove pages') + + click_link 'Remove pages' + + expect(project.pages_deployed?).to be_falsey + end + end + end + end end diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index c93b7cc6cac..95c2c122403 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,5 +1,3 @@ -import 'jquery'; -import 'jquery-ujs'; import AjaxLoadingSpinner from '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 1c1b3f3ced3..8e4bbb90ccb 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -138,7 +138,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.userAuthored($thumbsUpEmoji); - return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note"); + return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note"); }); it('should restore tooltip back to initial vote list', function() { var $thumbsUpEmoji, $votesBlock; @@ -149,7 +149,7 @@ import '~/lib/utils/common_utils'; awardsHandler.userAuthored($thumbsUpEmoji); jasmine.clock().tick(2801); jasmine.clock().uninstall(); - return expect($thumbsUpEmoji.data("original-title")).toBe("sam"); + return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam"); }); }); describe('::getAwardUrl', function() { @@ -194,7 +194,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -204,7 +204,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam'); }); }); describe('::removeYouToUserList', function() { @@ -217,7 +217,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -228,7 +228,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam'); }); }); describe('::searchEmojis', () => { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 8287c58ac5a..e500bbe750f 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -15,7 +15,7 @@ describe('requiresInput', () => { }); it('enables submit when no field is required', () => { - $('*[required=required]').removeAttr('required'); + $('*[required=required]').prop('required', false); $('.js-requires-input').requiresInput(); expect(submitButton).not.toBeDisabled(); }); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js index 6e1b0429ab7..f3f80cb3771 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -1,11 +1,13 @@ import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; import * as featureHighlight from '~/feature_highlight/feature_highlight'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; describe('feature highlight', () => { beforeEach(() => { setFixtures(` <div> - <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled> + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled> Trigger </div> </div> @@ -19,13 +21,21 @@ describe('feature highlight', () => { }); describe('setupFeatureHighlightPopover', () => { + let mock; const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/test').reply(200); spyOn(window, 'addEventListener'); spyOn(window, 'removeEventListener'); featureHighlight.setupFeatureHighlightPopover('test', 0); }); + afterEach(() => { + mock.restore(); + }); + it('setup popover content', () => { const $popoverContent = $('.feature-highlight-popover-content'); const outerHTML = $popoverContent.prop('outerHTML'); @@ -51,15 +61,6 @@ describe('feature highlight', () => { }, 0); }); - it('setup inserted.bs.popover', () => { - $(selector).trigger('mouseenter'); - const popoverId = $(selector).attr('aria-describedby'); - const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); - - $(`#${popoverId} .dismiss-feature-highlight`).click(); - expect(spyEvent).toHaveBeenTriggered(); - }); - it('setup show.bs.popover', () => { $(selector).trigger('show.bs.popover'); expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); @@ -75,9 +76,19 @@ describe('feature highlight', () => { }); it('displays popover', () => { - expect($(selector).attr('aria-describedby')).toBeFalsy(); + expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy(); $(selector).trigger('mouseenter'); - expect($(selector).attr('aria-describedby')).toBeTruthy(); + expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy(); + }); + + it('toggles when clicked', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + + expect(toggleSpy).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index f1e6119253e..c37a964975d 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -1,8 +1,6 @@ -import '~/filtered_search/dropdown_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown'; -import '~/filtered_search/dropdown_user'; - +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import DropdownUser from '~/filtered_search/dropdown_user'; +import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; describe('Dropdown User', () => { @@ -10,18 +8,18 @@ describe('Dropdown User', () => { let dropdownUser; beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); - spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); + spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {}); - dropdownUser = new gl.DropdownUser({ + dropdownUser = new DropdownUser({ tokenKeys: FilteredSearchTokenKeys, }); }); it('should not return the double quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ lastToken: '"johnny appleseed', }); @@ -29,7 +27,7 @@ describe('Dropdown User', () => { }); it('should not return the single quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ lastToken: '\'larry boy', }); @@ -39,22 +37,22 @@ describe('Dropdown User', () => { describe('config AjaxFilter\'s endpoint', () => { beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { window.gon = { relative_url_root: '', }; - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); it('should return endpoint when relative_url_root is undefined', () => { - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); @@ -63,7 +61,7 @@ describe('Dropdown User', () => { window.gon = { relative_url_root: '/gitlab_directory', }; - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); }); @@ -84,7 +82,7 @@ describe('Dropdown User', () => { loadFixtures(fixtureTemplate); authorFilterDropdownElement = document.querySelector('#js-dropdown-author'); const dummyInput = document.createElement('div'); - dropdown = new gl.DropdownUser({ + dropdown = new DropdownUser({ dropdown: authorFilterDropdownElement, input: dummyInput, }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index d6e1af105f1..3d6dec19eca 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,6 +1,5 @@ -import '~/filtered_search/dropdown_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; @@ -10,25 +9,25 @@ describe('Dropdown Utils', () => { describe('getEscapedText', () => { it('should return same word when it has no space', () => { - const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + const escaped = DropdownUtils.getEscapedText('textWithoutSpace'); expect(escaped).toBe('textWithoutSpace'); }); it('should escape with double quotes', () => { - let escaped = gl.DropdownUtils.getEscapedText('text with space'); + let escaped = DropdownUtils.getEscapedText('text with space'); expect(escaped).toBe('"text with space"'); - escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + escaped = DropdownUtils.getEscapedText('won\'t fix'); expect(escaped).toBe('"won\'t fix"'); }); it('should escape with single quotes', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + const escaped = DropdownUtils.getEscapedText('won"t fix'); expect(escaped).toBe('\'won"t fix\''); }); it('should escape with single quotes by default', () => { - const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + const escaped = DropdownUtils.getEscapedText('won"t\' fix'); expect(escaped).toBe('\'won"t\' fix\''); }); }); @@ -50,14 +49,14 @@ describe('Dropdown Utils', () => { it('should filter without symbol', () => { input.value = 'roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + const updatedItem = DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with symbol', () => { input.value = '@roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + const updatedItem = DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); @@ -69,56 +68,56 @@ describe('Dropdown Utils', () => { it('should filter with double quote', () => { input.value = '"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and symbol', () => { input.value = '~"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and multiple words', () => { input.value = '"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote, symbol and multiple words', () => { input.value = '~"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote', () => { input.value = '\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and symbol', () => { input.value = '~\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and multiple words', () => { input.value = '\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote, symbol and multiple words', () => { input.value = '~\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); }); @@ -150,26 +149,26 @@ describe('Dropdown Utils', () => { it('should filter', () => { input.value = 'l'; - let updatedItem = gl.DropdownUtils.filterHint(config(), { + let updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(false); input.value = 'o'; - updatedItem = gl.DropdownUtils.filterHint(config(), { + updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint(config(), {}, ''); + const updatedItem = DropdownUtils.filterHint(config(), {}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); it('should allow multiple if item.type is array', () => { input.value = 'label:~first la'; - const updatedItem = gl.DropdownUtils.filterHint(config(), { + const updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', type: 'array', }); @@ -178,12 +177,12 @@ describe('Dropdown Utils', () => { it('should prevent multiple if item.type is not array', () => { input.value = 'milestone:~first mile'; - let updatedItem = gl.DropdownUtils.filterHint(config(), { + let updatedItem = DropdownUtils.filterHint(config(), { hint: 'milestone', }); expect(updatedItem.droplab_hidden).toBe(true); - updatedItem = gl.DropdownUtils.filterHint(config(), { + updatedItem = DropdownUtils.filterHint(config(), { hint: 'milestone', type: 'string', }); @@ -205,7 +204,7 @@ describe('Dropdown Utils', () => { color: '#000000', }; - const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, newLabel); + const updated = DropdownUtils.mergeDuplicateLabels(dataMap, newLabel); expect(updated[newLabel.title]).toEqual(newLabel); }); @@ -215,36 +214,36 @@ describe('Dropdown Utils', () => { color: '#000000', }; - const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, duplicate); + const updated = DropdownUtils.mergeDuplicateLabels(dataMap, duplicate); expect(updated.label.multipleColors).toEqual([dataMap.label.color, duplicate.color]); }); }); describe('duplicateLabelColor', () => { it('should linear-gradient 2 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 50%, #000000 50%, #000000 100%)'); }); it('should linear-gradient 3 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 33%, #000000 33%, #000000 66%, #333333 66%, #333333 100%)'); }); it('should linear-gradient 4 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 25%, #000000 25%, #000000 50%, #333333 50%, #333333 75%, #DDDDDD 75%, #DDDDDD 100%)'); }); it('should not linear-gradient more than 4 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']); expect(gradient.indexOf('#EEEEEE') === -1).toEqual(true); }); }); describe('duplicateLabelPreprocessing', () => { it('should set preprocessed to true', () => { - const results = gl.DropdownUtils.duplicateLabelPreprocessing([]); + const results = DropdownUtils.duplicateLabelPreprocessing([]); expect(results.preprocessed).toEqual(true); }); @@ -256,7 +255,7 @@ describe('Dropdown Utils', () => { title: 'label2', color: '#000000', }]; - const results = gl.DropdownUtils.duplicateLabelPreprocessing(data); + const results = DropdownUtils.duplicateLabelPreprocessing(data); expect(results.length).toEqual(2); expect(results[0]).toEqual(data[0]); @@ -271,14 +270,14 @@ describe('Dropdown Utils', () => { title: 'label', color: '#000000', }]; - const results = gl.DropdownUtils.duplicateLabelPreprocessing(data); + const results = DropdownUtils.duplicateLabelPreprocessing(data); it('should merge duplicate labels', () => { expect(results.length).toEqual(1); }); it('should convert multiple colored labels into linear-gradient', () => { - expect(results[0].color).toEqual(gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000'])); + expect(results[0].color).toEqual(DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000'])); }); it('should set multiple colored label text color to black', () => { @@ -289,7 +288,7 @@ describe('Dropdown Utils', () => { describe('setDataValueIfSelected', () => { beforeEach(() => { - spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + spyOn(FilteredSearchDropdownManager, 'addWordToInput') .and.callFake(() => {}); }); @@ -298,8 +297,8 @@ describe('Dropdown Utils', () => { getAttribute: () => 'value', }; - gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + DropdownUtils.setDataValueIfSelected(null, selected); + expect(FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); }); it('returns true when dataValue exists', () => { @@ -307,7 +306,7 @@ describe('Dropdown Utils', () => { getAttribute: () => 'value', }; - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + const result = DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(true); }); @@ -316,7 +315,7 @@ describe('Dropdown Utils', () => { getAttribute: () => null, }; - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + const result = DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(false); }); }); @@ -326,7 +325,7 @@ describe('Dropdown Utils', () => { const value = 'label:none '; it('should return selectionStart when cursor is at the trailing space', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 11, value, }); @@ -336,7 +335,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 0, value, }); @@ -346,7 +345,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the middle of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 7, value, }); @@ -356,7 +355,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 10, value, }); @@ -370,7 +369,7 @@ describe('Dropdown Utils', () => { const value = 'label:~"Community Contribution"'; it('should return input when cursor is after the first word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 17, value, }); @@ -380,7 +379,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is before the second word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 18, value, }); @@ -394,7 +393,7 @@ describe('Dropdown Utils', () => { const value = 'label:~"Community Contribution'; it('should return entire input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 0, value, }); @@ -404,7 +403,7 @@ describe('Dropdown Utils', () => { }); it('should return entire input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 30, value, }); @@ -434,7 +433,7 @@ describe('Dropdown Utils', () => { const valueContainer = authorToken.querySelector('.value-container'); valueContainer.dataset.originalValue = originalValue; - const searchQuery = gl.DropdownUtils.getSearchQuery(); + const searchQuery = DropdownUtils.getSearchQuery(); expect(searchQuery).toBe(' search term author:original dance'); }); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index 5c7e9115aac..71c14582329 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,6 +1,4 @@ -import '~/filtered_search/filtered_search_visual_tokens'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { beforeEach(() => { @@ -28,7 +26,7 @@ describe('Filtered Search Dropdown Manager', () => { describe('input has no existing value', () => { it('should add just tokenName', () => { - gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + FilteredSearchDropdownManager.addWordToInput('milestone'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -38,7 +36,7 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should add tokenName and tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + FilteredSearchDropdownManager.addWordToInput('label'); let token = document.querySelector('.tokens-container .js-visual-token'); @@ -46,9 +44,9 @@ describe('Filtered Search Dropdown Manager', () => { expect(token.querySelector('.name').innerText).toBe('label'); expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + FilteredSearchDropdownManager.addWordToInput('label', 'none'); // We have to get that reference again - // Because gl.FilteredSearchDropdownManager deletes the previous token + // Because FilteredSearchDropdownManager deletes the previous token token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -61,7 +59,7 @@ describe('Filtered Search Dropdown Manager', () => { describe('input has existing value', () => { it('should be able to just add tokenName', () => { setInputValue('a'); - gl.FilteredSearchDropdownManager.addWordToInput('author'); + FilteredSearchDropdownManager.addWordToInput('author'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -71,10 +69,10 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should replace tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('author'); + FilteredSearchDropdownManager.addWordToInput('author'); setInputValue('roo'); - gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + FilteredSearchDropdownManager.addWordToInput(null, '@root'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -85,10 +83,10 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should add tokenValues containing spaces', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + FilteredSearchDropdownManager.addWordToInput('label'); setInputValue('"test '); - gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); const token = document.querySelector('.tokens-container .js-visual-token'); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 0ed9a587dc1..95d02974bdc 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -5,9 +5,10 @@ import RecentSearchesServiceError from '~/filtered_search/services/recent_search import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/lib/utils/common_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; -import '~/filtered_search/filtered_search_manager'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Manager', () => { @@ -49,21 +50,21 @@ describe('Filtered Search Manager', () => { </div> `); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); }); const initializeManager = () => { /* eslint-disable jasmine/no-unsafe-spy */ - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); - spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); /* eslint-enable jasmine/no-unsafe-spy */ input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); manager.setup(); }; @@ -81,7 +82,7 @@ describe('Filtered Search Manager', () => { }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ @@ -93,7 +94,7 @@ describe('Filtered Search Manager', () => { describe('setup', () => { beforeEach(() => { - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); }); it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { @@ -108,7 +109,7 @@ describe('Filtered Search Manager', () => { describe('searchState', () => { beforeEach(() => { - spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'search').and.callFake(() => {}); initializeManager(); }); @@ -134,7 +135,7 @@ describe('Filtered Search Manager', () => { }; manager.searchState(e); - expect(gl.FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); + expect(FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); }); it('should call search when there is state', () => { @@ -149,7 +150,7 @@ describe('Filtered Search Manager', () => { }; manager.searchState(e); - expect(gl.FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); + expect(FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); }); }); @@ -251,44 +252,44 @@ describe('Filtered Search Manager', () => { }); it('removes last token', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); dispatchBackspaceEvent(input, 'keyup'); dispatchBackspaceEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); }); it('sets the input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); dispatchDeleteEvent(input, 'keyup'); dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(input.value).toEqual('~bug'); }); }); it('does not remove token or change input when there is existing input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); input.value = 'text'; dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('text'); }); it('does not remove previous token on single backspace press', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); input.value = 't'; dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('t'); }); }); @@ -309,7 +310,7 @@ describe('Filtered Search Manager', () => { describe('unselected token', () => { beforeEach(() => { - spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), @@ -380,16 +381,16 @@ describe('Filtered Search Manager', () => { describe('removeSelectedToken', () => { beforeEach(() => { - spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); - spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); - spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); initializeManager(); }); it('calls FilteredSearchVisualTokens.removeSelectedToken', () => { manager.removeSelectedToken(); - expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); }); it('calls handleInputPlaceholder', () => { @@ -421,12 +422,12 @@ describe('Filtered Search Manager', () => { manager.filteredSearchInput.value = inputValue; manager.filteredSearchInput.dispatchEvent(new Event('input')); - expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue); + expect(DropdownUtils.getSearchQuery()).toEqual(inputValue); manager.clearSearchButton.click(); expect(manager.filteredSearchInput.value).toEqual(''); - expect(gl.DropdownUtils.getSearchQuery()).toEqual(''); + expect(DropdownUtils.getSearchQuery()).toEqual(''); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index bf8b66f1110..465f5f79931 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,19 +1,19 @@ import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; -import '~/filtered_search/filtered_search_tokenizer'; +import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { const allowedKeys = FilteredSearchTokenKeys.getKeys(); describe('processTokens', () => { it('returns for input containing only search value', () => { - const results = gl.FilteredSearchTokenizer.processTokens('searchTerm', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('searchTerm', allowedKeys); expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(0); expect(results.lastToken).toBe(results.searchToken); }); it('returns for input containing only tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none', allowedKeys); expect(results.searchToken).toBe(''); expect(results.tokens.length).toBe(4); @@ -37,7 +37,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input starting with search value and ending with tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(1); @@ -48,7 +48,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input starting with tokens and ending with search value', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('assignee:@user searchTerm', allowedKeys); expect(results.searchToken).toBe('searchTerm'); @@ -60,7 +60,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input containing search value wrapped between tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); @@ -81,7 +81,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input containing search value in between tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3); @@ -101,14 +101,14 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('fake:token', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('fake:token', allowedKeys); expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token'); expect(results.tokens.length).toEqual(0); }); it('returns search value and token for mix of valid and invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('label:real fake:token', allowedKeys); expect(results.tokens.length).toEqual(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('real'); @@ -118,13 +118,13 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid symbols', () => { - const results = gl.FilteredSearchTokenizer.processTokens('std::includes', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('std::includes', allowedKeys); expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes'); }); it('removes duplicated values', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('label:~foo label:~foo', allowedKeys); expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('foo'); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 0684c3498a2..f1da5f81c0f 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -2,11 +2,12 @@ import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; -import '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import DropdownUtils from '~/filtered_search//dropdown_utils'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { - const subject = gl.FilteredSearchVisualTokens; + const subject = FilteredSearchVisualTokens; const findElements = (tokenElement) => { const tokenNameElement = tokenElement.querySelector('.name'); @@ -860,25 +861,25 @@ describe('Filtered Search Visual Tokens', () => { it('does not preprocess more than once', () => { let labels = []; - spyOn(gl.DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []); + spyOn(DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []); - labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - expect(gl.DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1); + expect(DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1); }); describe('not preprocessed before', () => { it('returns preprocessed labels', () => { let labels = []; expect(labels.preprocessed).not.toEqual(true); - labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); expect(labels.preprocessed).toEqual(true); }); it('overrides AjaxCache with preprocessed results', () => { spyOn(AjaxCache, 'override').and.callFake(() => {}); - gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, []); + FilteredSearchVisualTokens.preprocessLabel(endpoint, []); expect(AjaxCache.override.calls.count()).toEqual(1); }); }); @@ -926,7 +927,7 @@ describe('Filtered Search Visual Tokens', () => { }; const findLabel = tokenValue => labelData.find( - label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`, + label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`, ); it('updates the color of a label token', (done) => { diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index b13d1bf8dff..67b854f61c0 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -64,8 +64,8 @@ describe('glDropdown', function describeDropdown() { }); afterEach(() => { - $('body').unbind('keydown'); - this.dropdownContainerElement.unbind('keyup'); + $('body').off('keydown'); + this.dropdownContainerElement.off('keyup'); }); it('should open on click', () => { diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 6599839a526..d8a8c8cc260 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ import { scaleLinear, scaleTime } from 'd3-scale'; import { timeParse } from 'd3-time-format'; -import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; const d3 = { scaleLinear, scaleTime, timeParse }; diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js index 962423462e7..e03114c1cc5 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_spec.js @@ -1,5 +1,5 @@ -import ContributorsStatGraph from '~/graphs/stat_graph_contributors'; -import { ContributorsGraph } from '~/graphs/stat_graph_contributors_graph'; +import ContributorsStatGraph from '~/pages/projects/graphs/show/stat_graph_contributors'; +import { ContributorsGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; import { setLanguage } from '../helpers/locale_helper'; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 9b47ab62181..22a9afe1a9d 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,6 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; +import ContributorsStatGraphUtil from '~/pages/projects/graphs/show/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index 5d0ee91d977..0d16b23302f 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -23,7 +23,7 @@ describe('Merge request notes', () => { gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + window.gon.current_user_id = $('.note:last').data('authorId'); return new Notes('', []); }); @@ -76,7 +76,7 @@ describe('Merge request notes', () => { </form>`; setFixtures(diffsResponse.html + noteFormHtml); $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + window.gon.current_user_id = $('.note:last').data('authorId'); return new Notes('', []); }); diff --git a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index d2386077aa6..349549b9e1f 100644 --- a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -22,19 +22,19 @@ describe('Abuse Reports', () => { it('should truncate long messages', () => { const $longMessage = findMessage('LONG MESSAGE'); - expect($longMessage.data('original-message')).toEqual(jasmine.anything()); + expect($longMessage.data('originalMessage')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); it('should not truncate short messages', () => { const $shortMessage = findMessage('SHORT MESSAGE'); - expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); + expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything()); }); it('should allow clicking a truncated message to expand and collapse the full message', () => { const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); - expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); + expect($longMessage.data('originalMessage').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 8ce33d410a7..e0ea3649646 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); }); @@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - eventHub.$on('actionConfirmationModal', (data) => { - expect(data.id).toEqual(123); + eventHub.$on('openConfirmationModal', (data) => { + expect(data.pipelineId).toEqual(123); + expect(data.type).toEqual('explode'); }); component = new AsyncButtonComponent({ @@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index bc6413a159f..e58a8018ed5 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import Vue from 'vue'; -import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; +import PipelineMediator from '~/pipelines/pipeline_details_mediator'; describe('PipelineMdediator', () => { let mediator; diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/javascripts/pipelines/pipeline_store_spec.js index 85d13445b01..ab2287cc344 100644 --- a/spec/javascripts/pipelines/pipeline_store_spec.js +++ b/spec/javascripts/pipelines/pipeline_store_spec.js @@ -8,7 +8,6 @@ describe('Pipeline Store', () => { }); it('should set defaults', () => { - expect(store.state).toEqual({ pipeline: {} }); expect(store.state.pipeline).toEqual({}); }); diff --git a/spec/javascripts/projects/project_import_gitlab_project_spec.js b/spec/javascripts/projects/project_import_gitlab_project_spec.js index 2f1aae109e3..126f73103e0 100644 --- a/spec/javascripts/projects/project_import_gitlab_project_spec.js +++ b/spec/javascripts/projects/project_import_gitlab_project_spec.js @@ -10,7 +10,7 @@ describe('Import Gitlab project', () => { <input class="js-path-name" /> `); - projectImportGitlab.bindEvents(); + projectImportGitlab(); }); afterEach(() => { diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index c314ca8ab72..8731ce35d81 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -27,7 +27,7 @@ describe('New Project', () => { }); it('does not change project path for disabled $projectImportUrl', () => { - $projectImportUrl.attr('disabled', true); + $projectImportUrl.prop('disabled', true); projectNew.deriveProjectPathFromUrl($projectImportUrl); @@ -36,7 +36,7 @@ describe('New Project', () => { describe('for enabled $projectImportUrl', () => { beforeEach(() => { - $projectImportUrl.attr('disabled', false); + $projectImportUrl.prop('disabled', false); }); it('does not change project path if it is set by user', () => { diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 97f762d07a7..0da5d91e376 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -78,7 +78,7 @@ describe('SidebarMoveIssue', () => { this.sidebarMoveIssue.onConfirmClicked(); expect(this.mediator.moveIssue).toHaveBeenCalled(); - expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + expect(this.$confirmButton.prop('disabled')).toBeTruthy(); expect(this.$confirmButton.hasClass('is-loading')).toBe(true); }); @@ -93,7 +93,7 @@ describe('SidebarMoveIssue', () => { // Wait for the move issue request to fail setTimeout(() => { expect(window.Flash).toHaveBeenCalled(); - expect(this.$confirmButton.attr('disabled')).toBe(undefined); + expect(this.$confirmButton.prop('disabled')).toBeFalsy(); expect(this.$confirmButton.hasClass('is-loading')).toBe(false); done(); }); @@ -120,7 +120,7 @@ describe('SidebarMoveIssue', () => { this.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); - expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + expect(this.$confirmButton.prop('disabled')).toBeTruthy(); done(); }, 0); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 9b2a5379855..323b8a9572d 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,6 +1,6 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import 'jasmine-jquery'; +import 'vendor/jasmine-jquery'; import '~/commons'; import Vue from 'vue'; @@ -144,6 +144,9 @@ if (process.env.BABEL_ENV === 'coverage') { describe('Uncovered files', function () { const sourceFiles = require.context('~', true, /\.js$/); + + $.holdReady(true); + sourceFiles.keys().forEach(function (path) { // ignore if there is a matching spec file if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb new file mode 100644 index 00000000000..c76adcbe2f5 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb @@ -0,0 +1,262 @@ +require 'spec_helper' + +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + + before(:all) do + ensure_temporary_tracking_table_exists + end + + describe '#upload_path' do + def assert_upload_path(file_path, expected_upload_path) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.upload_path).to eq(expected_upload_path) + end + + context 'for an appearance logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') + end + end + + context 'for an appearance header_logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') + end + end + + context 'for a user avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') + end + end + + context 'for a group avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') + end + end + + context 'for a project avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the file path relative to the project directory in uploads' do + project = create_project + random_hex = SecureRandom.hex + + assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + end + end + end + + describe '#uploader' do + def assert_uploader(file_path, expected_uploader) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.uploader).to eq(expected_uploader) + end + + context 'for an appearance logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for an appearance header_logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') + end + end + + context 'for a user avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a group avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns FileUploader as a string' do + project = create_project + + assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + end + end + end + + describe '#model_type' do + def assert_model_type(file_path, expected_model_type) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_type).to eq(expected_model_type) + end + + context 'for an appearance logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for an appearance header_logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns Note as a string' do + assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') + end + end + + context 'for a user avatar file path' do + it 'returns User as a string' do + assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') + end + end + + context 'for a group avatar file path' do + it 'returns Namespace as a string' do + assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') + end + end + + context 'for a project avatar file path' do + it 'returns Project as a string' do + assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns Project as a string' do + project = create_project + + assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') + end + end + end + + describe '#model_id' do + def assert_model_id(file_path, expected_model_id) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_id).to eq(expected_model_id) + end + + context 'for an appearance logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) + end + end + + context 'for an appearance header_logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) + end + end + + context 'for a user avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a group avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the ID as a string' do + project = create_project + + assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) + end + end + end + + describe '#file_size' do + context 'for an appearance logo file path' do + let(:appearance) { create_or_update_appearance(logo: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project avatar file path' do + let(:project) { create_project(avatar: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:project) { create_project } + let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } + + before do + add_markdown_attachment(project) + end + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + end + + def create_untracked_file(path_relative_to_upload_dir) + described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") + end +end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index c8fa252439a..0d2074eed22 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' -# This migration is using UploadService, which sets uploads.secret that is only -# added to the DB schema in 20180129193323. Since the test isn't isolated, we -# just use the latest schema when testing this migration. -# Ideally, the test should not use factories nor UploadService, and rely on the -# `table` helper instead. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do - include TrackUntrackedUploadsHelpers +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers subject { described_class.new } - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - let!(:uploads) { described_class::Upload } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:notes) { table(:notes) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } before do ensure_temporary_tracking_table_exists @@ -19,30 +21,30 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user1) { create(:user, :with_avatar) } - let!(:user2) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :legacy_storage, :with_avatar) } - let!(:project2) { create(:project, :legacy_storage, :with_avatar) } + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user1) { create_user(avatar: true) } + let!(:user2) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let!(:project2) { create_project(avatar: true) } before do - UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload - UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload + add_markdown_attachment(project1) + add_markdown_attachment(project2) # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: appearance.uploads.first.path) - untracked_files_for_uploads.create!(path: appearance.uploads.last.path) - untracked_files_for_uploads.create!(path: user1.uploads.first.path) - untracked_files_for_uploads.create!(path: user2.uploads.first.path) - untracked_files_for_uploads.create!(path: project1.uploads.first.path) - untracked_files_for_uploads.create!(path: project2.uploads.first.path) - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) + untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") # Untrack 4 files - user2.uploads.delete_all - project2.uploads.delete_all # 2 files: avatar and a Markdown upload - appearance.uploads.where("path like '%header_logo%'").delete_all + get_uploads(user2, 'User').delete_all + get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload + get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all end it 'adds untracked files to the uploads table' do @@ -50,9 +52,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) end.to change { uploads.count }.from(4).to(8) - expect(user2.uploads.count).to eq(1) - expect(project2.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(project2, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'deletes rows after processing them' do @@ -66,9 +68,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra it 'does not create duplicate uploads of already tracked files' do subject.perform(1, untracked_files_for_uploads.last.id) - expect(user1.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'uses the start and end batch ids [only 1st half]' do @@ -80,11 +82,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(1) - expect(appearance.uploads.count).to eq(2) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(0) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(0) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -99,11 +101,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(0) - expect(appearance.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(0) + expect(get_uploads(appearance, 'Appearance').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(2) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -122,7 +124,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") expect(untracked_files_for_uploads.count).to eq(9) expect(uploads.count).to eq(4) @@ -133,7 +135,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" untracked_files_for_uploads.create!(path: bad_path) expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) @@ -152,363 +154,100 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra describe 'upload outcomes for each path pattern' do shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - model.uploads.delete_all + model_uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { model_uploads.count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(expected_upload_attrs) + expect(model_uploads.first.attributes).to include(expected_upload_attrs) end end context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: uploaded_file) } + let(:model) { create_or_update_appearance(logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: uploaded_file) } + let(:model) { create_or_update_appearance(header_logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for a pre-Markdown Note attachment file path' do - let(:model) { create(:note, :with_attachment) } - let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let(:model) { create_note(attachment: true) } + let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - Upload.where(model_type: 'Note', model_id: model.id).delete_all + get_uploads(model, 'Note').delete_all end # Can't use the shared example because Note doesn't have an `uploads` association it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) + end.to change { get_uploads(model, 'Note').count }.from(0).to(1) - expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) end end context 'for a user avatar file path' do - let(:model) { create(:user, :with_avatar) } + let(:model) { create_user(avatar: true) } + let(:model_uploads) { get_uploads(model, 'User') } it_behaves_like 'non_markdown_file' end context 'for a group avatar file path' do - let(:model) { create(:group, :with_avatar) } + let(:model) { create_group(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Namespace') } it_behaves_like 'non_markdown_file' end context 'for a project avatar file path' do - let(:model) { create(:project, :legacy_storage, :with_avatar) } + let(:model) { create_project(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Project') } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create(:project, :legacy_storage) } + let(:model) { create_project } before do # Upload the file - UploadService.new(model, uploaded_file, FileUploader).execute + add_markdown_attachment(model) # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") # Save the expected upload attributes - @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') + @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') # Untrack the file - model.reload.uploads.delete_all + get_uploads(model, 'Project').delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { get_uploads(model, 'Project').count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(@expected_upload_attrs) + expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) end end end end - -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do - include TrackUntrackedUploadsHelpers - - let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } - - before(:all) do - ensure_temporary_tracking_table_exists - end - - describe '#upload_path' do - def assert_upload_path(file_path, expected_upload_path) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.upload_path).to eq(expected_upload_path) - end - - context 'for an appearance logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') - end - end - - context 'for an appearance header_logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') - end - end - - context 'for a user avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') - end - end - - context 'for a group avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') - end - end - - context 'for a project avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the file path relative to the project directory in uploads' do - project = create(:project, :legacy_storage) - random_hex = SecureRandom.hex - - assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") - end - end - end - - describe '#uploader' do - def assert_uploader(file_path, expected_uploader) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.uploader).to eq(expected_uploader) - end - - context 'for an appearance logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for an appearance header_logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') - end - end - - context 'for a user avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a group avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns FileUploader as a string' do - project = create(:project, :legacy_storage) - - assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') - end - end - end - - describe '#model_type' do - def assert_model_type(file_path, expected_model_type) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_type).to eq(expected_model_type) - end - - context 'for an appearance logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for an appearance header_logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns Note as a string' do - assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') - end - end - - context 'for a user avatar file path' do - it 'returns User as a string' do - assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') - end - end - - context 'for a group avatar file path' do - it 'returns Namespace as a string' do - assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') - end - end - - context 'for a project avatar file path' do - it 'returns Project as a string' do - assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns Project as a string' do - project = create(:project, :legacy_storage) - - assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') - end - end - end - - describe '#model_id' do - def assert_model_id(file_path, expected_model_id) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_id).to eq(expected_model_id) - end - - context 'for an appearance logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) - end - end - - context 'for an appearance header_logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) - end - end - - context 'for a user avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a group avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the ID as a string' do - project = create(:project, :legacy_storage) - - assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) - end - end - end - - describe '#file_size' do - context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: uploaded_file) } - let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(appearance.logo.size) - end - end - - context 'for a project avatar file path' do - let(:project) { create(:project, :legacy_storage, avatar: uploaded_file) } - let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.avatar.size) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create(:project, :legacy_storage) } - let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } - - before do - UploadService.new(project, uploaded_file, FileUploader).execute - end - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.uploads.first.size) - end - end - end - - def create_untracked_file(path_relative_to_upload_dir) - described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") - end -end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index ca77e64ae40..35750d89c35 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do - include TrackUntrackedUploadsHelpers - include MigrationsHelpers - - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } around do |example| # Especially important so the follow-up migration does not get run @@ -15,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat shared_examples 'prepares the untracked_files_for_uploads table' do context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar, :legacy_storage) } - let(:project2) { create(:project) } # instantiate after enabling hashed_storage + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let(:project2) { create_project } # instantiate after enabling hashed_storage before do # Markdown upload before enabling hashed_storage - UploadService.new(project1, uploaded_file, FileUploader).execute - - stub_application_setting(hashed_storage_enabled: true) + add_markdown_attachment(project1) # Markdown upload after enabling hashed_storage - UploadService.new(project2, uploaded_file, FileUploader).execute + add_markdown_attachment(project2, hashed_storage: true) end it 'has a path field long enough for really long paths' do @@ -61,7 +65,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat it 'does not add hashed files to the untracked_files_for_uploads table' do described_class.new.perform - hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 475b5c5cfb2..b49ddbfc780 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -190,7 +190,7 @@ describe Gitlab::Checks::ChangeAccess do context 'with LFS not enabled' do it 'skips the validation' do - expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation) + expect_any_instance_of(Gitlab::Checks::CommitCheck).not_to receive(:validate) subject.exec end @@ -207,7 +207,7 @@ describe Gitlab::Checks::ChangeAccess do end end - context 'when change is sent by the author od the lock' do + context 'when change is sent by the author of the lock' do let(:user) { owner } it "doesn't raise any error" do diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 59e9e1cc94c..a6341cd509b 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -276,6 +276,7 @@ describe Gitlab::Git::Blob, seed_helper: true do expect(blobs.count).to eq(1) expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + expect(blobs).to be_an(Array) end it 'accepts blob IDs as a lazy enumerator' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 85e6efd7ca2..0b20a6349a2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -102,6 +102,10 @@ describe Gitlab::Git::Commit, seed_helper: true do expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end + it "returns an array of parent ids" do + expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array) + end + it "should return valid commit for tag" do expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0482c48bde8..d601a383a98 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -600,6 +600,33 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#branch_names_contains_sha' do + shared_examples 'returning the right branches' do + let(:head_id) { repository.rugged.head.target.oid } + let(:new_branch) { head_id } + + before do + repository.create_branch(new_branch, 'master') + end + + after do + repository.delete_branch(new_branch) + end + + it 'displays that branch' do + expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch) + end + end + + context 'when Gitaly is enabled' do + it_behaves_like 'returning the right branches' + end + + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'returning the right branches' + end + end + describe "#refs_hash" do subject { repository.refs_hash } @@ -1590,7 +1617,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expected_languages = [ { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, - { value: 7.9, label: "HTML", color: "#e44b23", highlight: "#e44b23" }, + { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } ] diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 186b2d9279d..215f1ecc9c5 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::GitAccessWiki do let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :wiki_repo) } let(:user) { create(:user) } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:redirected_path) { nil } @@ -48,6 +48,18 @@ describe Gitlab::GitAccessWiki do it 'give access to download wiki code' do expect { subject }.not_to raise_error end + + context 'when the wiki repository does not exist' do + it 'returns not found' do + wiki_repo = project.wiki.repository + FileUtils.rm_rf(wiki_repo.path) + + # Sanity check for rm_rf + expect(wiki_repo.exists?).to eq(false) + + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.') + end + end end context 'when wiki feature is disabled' do diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index cbc7ce1c1b0..c50e73cecfc 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -84,4 +84,30 @@ describe Gitlab::GitalyClient::RepositoryService do expect(client.has_local_branches?).to be(true) end end + + describe '#rebase_in_progress?' do + let(:rebase_id) { 1 } + + it 'sends a repository_rebase_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_rebase_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.rebase_in_progress?(rebase_id) + end + end + + describe '#squash_in_progress?' do + let(:squash_id) { 1 } + + it 'sends a repository_squash_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_squash_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.squash_in_progress?(squash_id) + end + end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 46a57e08963..5bedfc79dd3 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do import_source: 'foo/bar', repository_storage_path: 'foo', disk_path: 'foo', - repository: repository + repository: repository, + create_wiki: true ) end @@ -192,7 +193,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do expect(importer.import_wiki_repository).to eq(true) end - it 'marks the import as failed if an error was raised' do + it 'marks the import as failed and creates an empty repo if an error was raised' do expect(importer.gitlab_shell) .to receive(:import_repository) .and_raise(Gitlab::Shell::Error) @@ -201,6 +202,9 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do .to receive(:fail_import) .and_return(false) + expect(project) + .to receive(:create_wiki) + expect(importer.import_wiki_repository).to eq(false) end end diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb index b49bc5c328c..f8faeffb935 100644 --- a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb +++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb @@ -1,19 +1,34 @@ require 'spec_helper' describe Gitlab::QueryLimiting::ActiveSupportSubscriber do + let(:transaction) { instance_double(Gitlab::QueryLimiting::Transaction, increment: true) } + + before do + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + end + describe '#sql' do it 'increments the number of executed SQL queries' do - transaction = double(:transaction) - - allow(Gitlab::QueryLimiting::Transaction) - .to receive(:current) - .and_return(transaction) + User.count expect(transaction) - .to receive(:increment) - .at_least(:once) + .to have_received(:increment) + .once + end - User.count + context 'when the query is actually a rails cache hit' do + it 'does not increment the number of executed SQL queries' do + ActiveRecord::Base.connection.cache do + User.count + User.count + end + + expect(transaction) + .to have_received(:increment) + .once + end end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 59eda025108..bcbb9287199 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -463,59 +463,30 @@ describe Notify do end describe 'project access requested' do - context 'for a project in a user namespace' do - let(:project) do - create(:project, :public, :access_requestable) do |project| - project.add_master(project.owner, current_user: project.owner) - end - end - - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) - - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) end end - context 'for a project in a group' do - let(:group_owner) { create(:user) } - let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } - let(:project) { create(:project, :public, :access_requestable, namespace: group) } - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } + let(:project_member) do + project.request_access(user) + project.requesters.find_by(user_id: user.id) + end + subject { described_class.member_access_requested_email('project', project_member.id, recipient.notification_email) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) + it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access - end + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_body_text project_project_members_url(project) + is_expected.to have_body_text project_member.human_access end end @@ -959,13 +930,16 @@ describe Notify do group.request_access(user) group.requesters.find_by(user_id: user.id) end - subject { described_class.member_access_requested_email('group', group_member.id) } + subject { described_class.member_access_requested_email('group', group_member.id, recipient.notification_email) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) + is_expected.to have_subject "Request to join the #{group.name} group" is_expected.to have_html_escaped_body_text group.name is_expected.to have_body_text group_group_members_url(group) diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index fe4d5b8a279..2fccfb3f12c 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') describe TrackUntrackedUploads, :migration, :sidekiq do - include TrackUntrackedUploadsHelpers + include MigrationsHelpers::TrackUntrackedUploadsHelpers it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 31959d28fee..ad3eec88952 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -698,6 +698,7 @@ describe API::Commits do get api(route, current_user) expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers expect(json_response.size).to be >= 1 expect(json_response.first.keys).to include 'diff' end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index c7df6251d74..827f4c04324 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Internal do let(:user) { create(:user) } let(:key) { create(:key, user: user) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :wiki_repo) } let(:secret_token) { Gitlab::Shell.secret_token } let(:gl_repository) { "project-#{project.id}" } let(:reference_counter) { double('ReferenceCounter') } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 13bb3b92085..e6d7b9fde02 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Issues, :mailer do +describe API::Issues do set(:user) { create(:user) } set(:project) do create(:project, :public, creator_id: user.id, namespace: user.namespace) @@ -932,18 +932,6 @@ describe API::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' expect(response).to have_gitlab_http_status(400) @@ -1246,18 +1234,6 @@ describe API::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: '' diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 5d01dc37f0e..025165622b7 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe API::PagesDomains do - set(:project) { create(:project) } + set(:project) { create(:project, path: 'my.project') } set(:user) { create(:user) } set(:admin) { create(:admin) } @@ -16,6 +16,7 @@ describe API::PagesDomains do let(:route) { "/projects/#{project.id}/pages/domains" } let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } + let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } @@ -144,6 +145,16 @@ describe API::PagesDomains do expect(json_response['certificate']).to be_nil end + it 'returns pages domain with project path' do + get api(route_domain_path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') + expect(json_response['domain']).to eq(pages_domain.domain) + expect(json_response['url']).to eq(pages_domain.url) + expect(json_response['certificate']).to be_nil + end + it 'returns pages domain with a certificate' do get api(route_secure_domain, user) diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 25b4c8438e4..0e745c82395 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::V3::Issues, :mailer do +describe API::V3::Issues do set(:user) { create(:user) } set(:user2) { create(:user) } set(:non_member) { create(:user) } @@ -780,18 +780,6 @@ describe API::V3::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' @@ -1045,18 +1033,6 @@ describe API::V3::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 942e5b2bb1b..c6fdda203ad 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -150,7 +150,7 @@ describe 'Git HTTP requests' do let(:path) { "/#{wiki.repository.full_path}.git" } context "when the project is public" do - let(:project) { create(:project, :repository, :public, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled) } it_behaves_like 'pushes require Basic HTTP Authentication' @@ -177,7 +177,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled) } it_behaves_like 'pulls are allowed' it_behaves_like 'pushes are allowed' @@ -198,7 +198,7 @@ describe 'Git HTTP requests' do end context "when the project is private" do - let(:project) { create(:project, :repository, :private, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled) } it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication' @@ -210,7 +210,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) } it 'allows clones' do download(path, user: user.username, password: user.password) do |response| diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35eb84e5e88..836ffb7cea0 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1307,6 +1307,33 @@ describe NotificationService, :mailer do end describe 'GroupMember' do + let(:added_user) { create(:user) } + + describe '#new_access_request' do + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:developer) { create(:user) } + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_owner(owner) + group.add_master(master) + group.add_developer(developer) + end + end + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + group.request_access(added_user) + + should_email(owner) + should_email(master) + should_not_email(developer) + end + end + describe '#decline_group_invite' do let(:creator) { create(:user) } let(:group) { create(:group) } @@ -1328,18 +1355,9 @@ describe NotificationService, :mailer do describe '#new_group_member' do let(:group) { create(:group) } - let(:added_user) { create(:user) } - - def create_member! - GroupMember.create( - group: group, - user: added_user, - access_level: Gitlab::Access::GUEST - ) - end it 'sends a notification' do - create_member! + group.add_guest(added_user) should_only_email(added_user) end @@ -1349,7 +1367,7 @@ describe NotificationService, :mailer do end it 'does not send a notification' do - create_member! + group.add_guest(added_user) should_not_email_anyone end end @@ -1357,8 +1375,42 @@ describe NotificationService, :mailer do end describe 'ProjectMember' do + let(:project) { create(:project) } + set(:added_user) { create(:user) } + + describe '#new_access_request' do + context 'for a project in a user namespace' do + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) + end + end + + it 'sends notification to project owners_and_masters' do + project.request_access(added_user) + + should_only_email(project.owner) + end + end + + context 'for a project in a group' do + let(:group_owner) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } + let!(:project) { create(:project, :public, :access_requestable, namespace: group) } + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + project.request_access(added_user) + + should_only_email(group_owner) + end + end + end + describe '#decline_group_invite' do - let(:project) { create(:project) } let(:member) { create(:user) } before do @@ -1375,19 +1427,12 @@ describe NotificationService, :mailer do end describe '#new_project_member' do - let(:project) { create(:project) } - let(:added_user) { create(:user) } - - def create_member! - create(:project_member, user: added_user, project: project) - end - it do create_member! should_only_email(added_user) end - describe 'when notifications are disabled' do + context 'when notifications are disabled' do before do create_global_setting_for(added_user, :disabled) end @@ -1398,6 +1443,10 @@ describe NotificationService, :mailer do end end end + + def create_member! + create(:project_member, user: added_user, project: project) + end end context 'guest user in private project' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85de0a14631..5600c9c6ad5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -154,6 +154,22 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + # The :each scope runs "inside" the example, so this hook ensures the DB is in the + # correct state before any examples' before hooks are called. This prevents a + # problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends + # on background migrations being run inline during test setup) can be broken by + # altering Sidekiq behavior in an unrelated spec like so: + # + # around do |example| + # Sidekiq::Testing.fake! do + # example.run + # end + # end + config.before(:context, :migration) do + schema_migrate_down! + end + + # Each example may call `migrate!`, so we must ensure we are migrated down every time config.before(:each, :migration) do schema_migrate_down! end diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb new file mode 100644 index 00000000000..016bcfa9b1b --- /dev/null +++ b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb @@ -0,0 +1,128 @@ +module MigrationsHelpers + module TrackUntrackedUploadsHelpers + PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') + UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') + SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') + UPLOAD_FILENAME = 'image.png'.freeze + FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') + FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze + + def create_or_update_appearance(logo: false, header_logo: false) + appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) + + add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo + add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo + + appearance + end + + def create_group(avatar: false) + index = unique_index(:group) + group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) + + add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar + + group + end + + def create_note(attachment: false) + note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) + + add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment + + note + end + + def create_project(avatar: false) + group = create_group + project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) + routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works + + add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar + + project + end + + def create_user(avatar: false) + user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) + + add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar + + user + end + + def unique_index(name = :unnamed) + @unique_index ||= {} + @unique_index[name] ||= 0 + @unique_index[name] += 1 + end + + def add_upload(model, model_type, attachment_type, uploader) + file_path = upload_file_path(model, model_type, attachment_type) + path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') + create_file(file_path) + + uploads.create!( + size: 1062, + path: path_relative_to_public, + model_id: model.id, + model_type: model_type == 'Group' ? 'Namespace' : model_type, + uploader: uploader, + checksum: FIXTURE_CHECKSUM + ) + end + + def add_markdown_attachment(project, hashed_storage: false) + project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) + attachment_dir = File.join(project_dir, SecureRandom.hex) + attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) + project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') + create_file(attachment_file_path) + + uploads.create!( + size: 1062, + path: project_attachment_path_relative_to_project, + model_id: project.id, + model_type: 'Project', + uploader: 'FileUploader', + checksum: FIXTURE_CHECKSUM + ) + end + + def legacy_project_uploads_dir(project) + namespace = namespaces.find_by(id: project.namespace_id) + File.join(UPLOADS_DIR, namespace.path, project.path) + end + + def hashed_project_uploads_dir(project) + File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') + end + + def upload_file_path(model, model_type, attachment_type) + dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) + File.join(dir, UPLOAD_FILENAME) + end + + def upload_dir(model_type, attachment_type) + File.join(SYSTEM_DIR, model_type, attachment_type) + end + + def create_file(path) + File.delete(path) if File.exist?(path) + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(FIXTURE_FILE_PATH, path) + end + + def get_uploads(model, model_type) + uploads.where(model_type: model_type, model_id: model.id) + end + + def get_full_path(project) + routes.find_by(source_id: project.id, source_type: 'Project').path + end + + def ensure_temporary_tracking_table_exists + Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c275522159c..01321989f01 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -1,5 +1,5 @@ require 'rspec/mocks' -require 'toml' +require 'toml-rb' module TestEnv extend self diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb deleted file mode 100644 index a8b3ed1f41c..00000000000 --- a/spec/support/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,16 +0,0 @@ -module TrackUntrackedUploadsHelpers - def uploaded_file - fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg') - fixture_file_upload(fixture_path) - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - - def create_or_update_appearance(attrs) - a = Appearance.first_or_initialize(title: 'foo', description: 'bar') - a.update!(attrs) - a - end -end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index b37d6ac831f..1f4053ff9ad 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -132,7 +132,7 @@ describe 'gitlab:gitaly namespace rake task' do expect { run_rake_task('gitlab:gitaly:storage_config')} .to output(expected_output).to_stdout - parsed_output = TOML.parse(expected_output) + parsed_output = TomlRB.parse(expected_output) config.each do |name, params| expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] }) end |