diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-24 15:09:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-24 15:09:37 +0300 |
commit | 6f15c2c2723dbe14e907379b75201e2ef70a48d3 (patch) | |
tree | f917af54164003d021a4af518ced7271978c5537 /spec | |
parent | 7a8d983c19c9fe14e7c0b8b6256b8cbacbff1959 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
38 files changed, 667 insertions, 628 deletions
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index c72d9e5053a..9b09f46d17e 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -260,6 +260,17 @@ RSpec.describe Boards::ListsController do end end + context 'with an error service response' do + it 'returns an unprocessable entity response' do + allow(Boards::Lists::DestroyService).to receive(:new) + .and_return(double(execute: ServiceResponse.error(message: 'error'))) + + remove_board_list user: user, board: board, list: planning + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + def remove_board_list(user:, board:, list:) sign_in(user) diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 35d8c0b7c6d..9a3df819656 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -2,18 +2,18 @@ require 'spec_helper' -RSpec.describe GroupsController do +RSpec.describe GroupsController, factory_default: :keep do include ExternalAuthorizationServiceHelpers - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group, :public) } - let(:project) { create(:project, namespace: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let!(:owner) { group.add_owner(create(:user)).user } - let!(:maintainer) { group.add_maintainer(create(:user)).user } - let!(:developer) { group.add_developer(create(:user)).user } - let!(:guest) { group.add_guest(create(:user)).user } + let_it_be_with_refind(:group) { create_default(:group, :public) } + let_it_be_with_refind(:project) { create(:project, namespace: group) } + let_it_be(:user) { create(:user) } + let_it_be(:admin) { create(:admin) } + let_it_be(:group_member) { create(:group_member, group: group, user: user) } + let_it_be(:owner) { group.add_owner(create(:user)).user } + let_it_be(:maintainer) { group.add_maintainer(create(:user)).user } + let_it_be(:developer) { group.add_developer(create(:user)).user } + let_it_be(:guest) { group.add_guest(create(:user)).user } shared_examples 'member with ability to create subgroups' do it 'renders the new page' do @@ -57,7 +57,6 @@ RSpec.describe GroupsController do describe 'GET #show' do before do sign_in(user) - project end let(:format) { :html } @@ -82,7 +81,6 @@ RSpec.describe GroupsController do describe 'GET #details' do before do sign_in(user) - project end let(:format) { :html } @@ -131,12 +129,9 @@ RSpec.describe GroupsController do end describe 'GET #activity' do - render_views - context 'as json' do before do sign_in(user) - project end it 'includes events from all projects in group and subgroups', :sidekiq_might_not_need_inline do @@ -157,10 +152,6 @@ RSpec.describe GroupsController do end context 'when user has no permission to see the event' do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } - let(:project_with_restricted_access) do create(:project, :public, issues_access_level: ProjectFeature::PRIVATE, group: group) end @@ -552,8 +543,6 @@ RSpec.describe GroupsController do end context 'when there is a conflicting group path' do - render_views - let!(:conflict_group) { create(:group, path: SecureRandom.hex(12) ) } let!(:old_name) { group.name } @@ -794,6 +783,7 @@ RSpec.describe GroupsController do context 'when transferring to a subgroup goes right' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } @@ -805,11 +795,8 @@ RSpec.describe GroupsController do } end - it 'returns a notice' do + it 'returns a notice and redirects to the new path' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") - end - - it 'redirects to the new path' do expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}") end end @@ -826,17 +813,15 @@ RSpec.describe GroupsController do } end - it 'returns a notice' do + it 'returns a notice and redirects to the new path' do expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.") - end - - it 'redirects to the new path' do expect(response).to redirect_to("/#{group.path}") end end context 'When the transfer goes wrong' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) } @@ -850,17 +835,15 @@ RSpec.describe GroupsController do } end - it 'returns an alert' do + it 'returns an alert and redirects to the current path' do expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved" - end - - it 'redirects to the current path' do expect(response).to redirect_to(edit_group_path(group)) end end context 'when the user is not allowed to transfer the group' do let(:new_parent_group) { create(:group, :public) } + let(:group) { create(:group, :public) } let!(:group_member) { create(:group_member, :guest, group: group, user: user) } let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) } @@ -879,6 +862,7 @@ RSpec.describe GroupsController do context 'transferring when a project has container images' do let(:group) { create(:group, :public, :nested) } + let(:project) { create(:project, namespace: group) } let!(:group_member) { create(:group_member, :owner, group: group, user: user) } before do @@ -979,6 +963,8 @@ RSpec.describe GroupsController do end context 'when there is no file available to download' do + let(:admin) { create(:admin) } + before do sign_in(admin) end @@ -1149,9 +1135,7 @@ RSpec.describe GroupsController do describe "GET #activity as JSON" do include DesignManagementTestHelpers - render_views - let(:project) { create(:project, :public, group: group) } let(:other_project) { create(:project, :public, group: group) } def get_activity diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index cc866d336a4..3397277839e 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -9,7 +9,7 @@ FactoryBot.define do content { 'Content for wiki page' } format { :markdown } message { nil } - project { association(:project, :wiki_repo) } + project { association(:project) } container { project } wiki { association(:wiki, container: container) } page { OpenStruct.new(url_path: title) } @@ -18,6 +18,7 @@ FactoryBot.define do initialize_with do new(wiki, page).tap do |page| page.attributes = { + slug: title&.tr(' ', '-'), title: title, content: content, format: format diff --git a/spec/factories/wikis.rb b/spec/factories/wikis.rb index 96578fdcee6..64f7a72990d 100644 --- a/spec/factories/wikis.rb +++ b/spec/factories/wikis.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :wiki do transient do - container { association(:project, :wiki_repo) } + container { association(:project) } user { association(:user) } end @@ -12,7 +12,7 @@ FactoryBot.define do factory :project_wiki do transient do - project { association(:project, :wiki_repo) } + project { association(:project) } end container { project } diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index e0a6e0fec1b..62e292c557c 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -6,7 +6,9 @@ RSpec.describe 'GFM autocomplete', :js do let_it_be(:user_xss_title) { 'eve <img src=x onerror=alert(2)<img src=x onerror=alert(1)>' } let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') } let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } - let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group, name: 'Ancestor') } + let_it_be(:child_group) { create(:group, parent: group, name: 'My group') } + let_it_be(:project) { create(:project, group: child_group) } let_it_be(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } @@ -535,7 +537,7 @@ RSpec.describe 'GFM autocomplete', :js do expect(page).to have_selector('.tribute-container', visible: true) - expect(find('.tribute-container ul', visible: true).text).to have_content(user_xss.username) + expect(find('.tribute-container ul', visible: true)).to have_text(user_xss.username) end it 'selects the first item for assignee dropdowns' do @@ -563,6 +565,24 @@ RSpec.describe 'GFM autocomplete', :js do expect(find('.tribute-container ul', visible: true)).to have_content(user.name) end + context 'when autocompleting for groups' do + it 'shows the group when searching for the name of the group' do + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('@mygroup') + end + + expect(find('.tribute-container ul', visible: true)).to have_text('My group') + end + + it 'does not show the group when searching for the name of the parent of the group' do + page.within '.timeline-content-form' do + find('#note-body').native.send_keys('@ancestor') + end + + expect(find('.tribute-container ul', visible: true)).not_to have_text('My group') + end + end + context 'if a selected value has special characters' do it 'wraps the result in double quotes' do note = find('#note-body') diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb index 2a3f7d62828..a98bfd1c8a4 100644 --- a/spec/features/merge_request/maintainer_edits_fork_spec.rb +++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb @@ -26,10 +26,12 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork visit project_merge_request_path(target_project, merge_request) click_link 'Changes' wait_for_requests - within first('.js-file-title') do - find('[data-testid="edit_file"]').click - click_link 'Edit in single-file editor' + + page.within(first('.js-file-title')) do + find('.js-diff-more-actions').click + find('.js-edit-blob').click end + wait_for_requests end diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 3a199951b56..ad1ad067935 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -34,7 +34,8 @@ RSpec.describe 'User comments on a diff', :js do page.within('.diff-files-holder > div:nth-child(3)') do expect(page).to have_content('Line is wrong') - find('.js-btn-vue-toggle-comments').click + find('.js-diff-more-actions').click + click_button 'Hide comments on this file' expect(page).not_to have_content('Line is wrong') end @@ -67,7 +68,8 @@ RSpec.describe 'User comments on a diff', :js do # Hide the comment. page.within('.diff-files-holder > div:nth-child(3)') do - find('.js-btn-vue-toggle-comments').click + find('.js-diff-more-actions').click + click_button 'Hide comments on this file' expect(page).not_to have_content('Line is wrong') end @@ -80,7 +82,8 @@ RSpec.describe 'User comments on a diff', :js do # Show the comment. page.within('.diff-files-holder > div:nth-child(3)') do - find('.js-btn-vue-toggle-comments').click + find('.js-diff-more-actions').click + click_button 'Show comments on this file' end # Now both the comments should be shown. diff --git a/spec/features/merge_request/user_sees_diff_spec.rb b/spec/features/merge_request/user_sees_diff_spec.rb index b4c06535b68..a7713ed9964 100644 --- a/spec/features/merge_request/user_sees_diff_spec.rb +++ b/spec/features/merge_request/user_sees_diff_spec.rb @@ -63,7 +63,7 @@ RSpec.describe 'Merge request > User sees diff', :js do visit diffs_project_merge_request_path(project, merge_request) # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax - expect(page).to have_selector("[id=\"#{changelog_id}\"] [data-testid='edit_file']") + expect(page).to have_selector("[id=\"#{changelog_id}\"] .js-edit-blob", visible: false) end end @@ -73,7 +73,8 @@ RSpec.describe 'Merge request > User sees diff', :js do visit diffs_project_merge_request_path(project, merge_request) # Throws `Capybara::Poltergeist::InvalidSelector` if we try to use `#hash` syntax - find("[id=\"#{changelog_id}\"] [data-testid=\"edit_file\"").click + find("[id=\"#{changelog_id}\"] .js-diff-more-actions").click + find("[id=\"#{changelog_id}\"] .js-edit-blob").click expect(page).to have_selector('.js-fork-suggestion-button', count: 1) expect(page).to have_selector('.js-cancel-fork-suggestion-button', count: 1) diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb index fe3307c0ceb..abdbd3a6f8a 100644 --- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb +++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb @@ -119,7 +119,8 @@ RSpec.describe 'User comments on a diff', :js do it 'can add and remove suggestions from a batch' do files.each_with_index do |file, index| page.within("[id='#{file[:hash]}']") do - find("button[title='Show full file']").click + find('.js-diff-more-actions').click + click_button 'Show full file' wait_for_requests click_diff_line(find("[id='#{file[:line_code]}']")) diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 3949c70e718..90d9263b515 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -20,22 +20,14 @@ RSpec.describe 'Editing file blob', :js do sign_in(user) end - def edit_and_commit(commit_changes: true) + def edit_and_commit(commit_changes: true, is_diff: false) wait_for_requests - find('.js-edit-blob').click - fill_and_commit(commit_changes) - end - - def mr_edit_and_commit(commit_changes: true) - wait_for_requests - find('[data-testid="edit_file"]').click - click_link 'Edit in single-file editor' - - fill_and_commit(commit_changes) - end + if is_diff + first('.js-diff-more-actions').click + end - def fill_and_commit(commit_changes) + first('.js-edit-blob').click fill_editor(content: 'class NextFeature\\nend\\n') if commit_changes @@ -51,7 +43,7 @@ RSpec.describe 'Editing file blob', :js do context 'from MR diff' do before do visit diffs_project_merge_request_path(project, merge_request) - mr_edit_and_commit + edit_and_commit(is_diff: true) end it 'returns me to the mr' do diff --git a/spec/fixtures/lib/backup/project_repo.bundle b/spec/fixtures/lib/backup/project_repo.bundle Binary files differnew file mode 100644 index 00000000000..44d4fc56d51 --- /dev/null +++ b/spec/fixtures/lib/backup/project_repo.bundle diff --git a/spec/fixtures/lib/backup/wiki_repo.bundle b/spec/fixtures/lib/backup/wiki_repo.bundle Binary files differnew file mode 100644 index 00000000000..bcc08dcbe8e --- /dev/null +++ b/spec/fixtures/lib/backup/wiki_repo.bundle diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 3ae0d06162d..f7c6290ce1c 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -1152,4 +1152,44 @@ describe('Api', () => { }); }); }); + + describe('trackRedisHllUserEvent', () => { + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/usage_data/increment_unique_users`; + + const event = 'dummy_event'; + const postData = { event }; + const headers = { + 'Content-Type': 'application/json', + }; + + describe('when usage data increment unique users is called with feature flag disabled', () => { + beforeEach(() => { + gon.features = { ...gon.features, usageDataApi: false }; + }); + + it('returns null', () => { + jest.spyOn(axios, 'post'); + mock.onPost(expectedUrl).replyOnce(httpStatus.OK, true); + + expect(axios.post).toHaveBeenCalledTimes(0); + expect(Api.trackRedisHllUserEvent(event)).toEqual(null); + }); + }); + + describe('when usage data increment unique users is called', () => { + beforeEach(() => { + gon.features = { ...gon.features, usageDataApi: true }; + }); + + it('resolves the Promise', () => { + jest.spyOn(axios, 'post'); + mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true); + + return Api.trackRedisHllUserEvent(event).then(({ data }) => { + expect(data).toEqual(true); + expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, { headers }); + }); + }); + }); + }); }); diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js index 817cb8dbf0c..3a236228c40 100644 --- a/spec/frontend/diffs/components/diff_file_header_spec.js +++ b/spec/frontend/diffs/components/diff_file_header_spec.js @@ -1,9 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import Vuex from 'vuex'; -import { GlIcon } from '@gitlab/ui'; import { cloneDeep } from 'lodash'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import EditButton from '~/diffs/components/edit_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import diffDiscussionsMockData from '../mock_data/diff_discussions'; import { truncateSha } from '~/lib/utils/text_utility'; @@ -76,16 +74,7 @@ describe('DiffFileHeader component', () => { const findReplacedFileButton = () => wrapper.find({ ref: 'replacedFileButton' }); const findViewFileButton = () => wrapper.find({ ref: 'viewButton' }); const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' }); - const hasZDropdownMenuClass = () => wrapper.classes('gl-z-dropdown-menu!'); - - const findIconByName = iconName => { - const icons = wrapper.findAll(GlIcon).filter(w => w.props('name') === iconName); - if (icons.length === 0) return icons; - if (icons.length > 1) { - throw new Error(`Multiple icons found for ${iconName}`); - } - return icons.at(0); - }; + const findEditButton = () => wrapper.find({ ref: 'editButton' }); const createComponent = props => { mockStoreConfig = cloneDeep(defaultMockStoreConfig); @@ -152,10 +141,6 @@ describe('DiffFileHeader component', () => { expect(wrapper.find(ClipboardButton).exists()).toBe(true); }); - it('should not have z dropdown menu class', () => { - expect(hasZDropdownMenuClass()).toBe(false); - }); - describe('for submodule', () => { const submoduleDiffFile = { ...diffFile, @@ -208,16 +193,6 @@ describe('DiffFileHeader component', () => { describe('for any file', () => { const otherModes = Object.keys(diffViewerModes).filter(m => m !== 'mode_changed'); - it('when edit button emits showForkMessage event it is re-emitted', () => { - createComponent({ - addMergeRequestButtons: true, - }); - wrapper.find(EditButton).vm.$emit('showForkMessage'); - return wrapper.vm.$nextTick().then(() => { - expect(wrapper.emitted().showForkMessage).toBeDefined(); - }); - }); - it('for mode_changed file mode displays mode changes', () => { createComponent({ diffFile: { @@ -276,16 +251,16 @@ describe('DiffFileHeader component', () => { }); it('should not render edit button', () => { createComponent({ addMergeRequestButtons: false }); - expect(wrapper.find(EditButton).exists()).toBe(false); + expect(findEditButton().exists()).toBe(false); }); }); describe('when addMergeRequestButtons is true', () => { describe('without discussions', () => { - it('renders a disabled toggle discussions button', () => { + it('does not render a toggle discussions button', () => { diffHasDiscussionsResultMock.mockReturnValue(false); createComponent({ addMergeRequestButtons: true }); - expect(findToggleDiscussionsButton().attributes('disabled')).toBe('true'); + expect(findToggleDiscussionsButton().exists()).toBe(false); }); }); @@ -293,7 +268,7 @@ describe('DiffFileHeader component', () => { it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => { diffHasDiscussionsResultMock.mockReturnValue(true); createComponent({ addMergeRequestButtons: true }); - expect(findToggleDiscussionsButton().attributes('disabled')).toBeFalsy(); + expect(findToggleDiscussionsButton().exists()).toBe(true); findToggleDiscussionsButton().vm.$emit('click'); expect( mockStoreConfig.modules.diffs.actions.toggleFileDiscussionWrappers, @@ -305,28 +280,7 @@ describe('DiffFileHeader component', () => { createComponent({ addMergeRequestButtons: true, }); - expect(wrapper.find(EditButton).exists()).toBe(true); - }); - - describe('when edit button opens', () => { - beforeEach(async () => { - createComponent({ addMergeRequestButtons: true }); - wrapper.find(EditButton).vm.$emit('open'); - - await wrapper.vm.$nextTick(); - }); - - it('should add z dropdown menu class when edit button opens', async () => { - expect(hasZDropdownMenuClass()).toBe(true); - }); - - it('when closes again, should remove class', async () => { - wrapper.find(EditButton).vm.$emit('close'); - - await wrapper.vm.$nextTick(); - - expect(hasZDropdownMenuClass()).toBe(false); - }); + expect(findEditButton().exists()).toBe(true); }); describe('view on environment button', () => { @@ -360,7 +314,7 @@ describe('DiffFileHeader component', () => { }); it('should not render edit button', () => { - expect(wrapper.find(EditButton).exists()).toBe(false); + expect(findEditButton().exists()).toBe(false); }); }); describe('with file blob', () => { @@ -371,7 +325,7 @@ describe('DiffFileHeader component', () => { addMergeRequestButtons: true, }); expect(findViewFileButton().attributes('href')).toBe(viewPath); - expect(findViewFileButton().attributes('title')).toEqual( + expect(findViewFileButton().text()).toEqual( `View file @ ${diffFile.content_sha.substr(0, 8)}`, ); }); @@ -401,21 +355,6 @@ describe('DiffFileHeader component', () => { addMergeRequestButtons: true, }; - it.each` - iconName | isShowingFullFile - ${'doc-expand'} | ${false} - ${'doc-changes'} | ${true} - `( - 'shows $iconName when isShowingFullFile set to $isShowingFullFile', - ({ iconName, isShowingFullFile }) => { - createComponent({ - ...fullyNotExpandedFileProps, - diffFile: { ...fullyNotExpandedFileProps.diffFile, isShowingFullFile }, - }); - expect(findIconByName(iconName).exists()).toBe(true); - }, - ); - it('renders expand to full file button if not showing full file already', () => { createComponent(fullyNotExpandedFileProps); expect(findExpandButton().exists()).toBe(true); @@ -481,7 +420,7 @@ describe('DiffFileHeader component', () => { it('does not show edit button', () => { createComponent({ diffFile: { ...diffFile, deleted_file: true } }); - expect(wrapper.find(EditButton).exists()).toBe(false); + expect(findEditButton().exists()).toBe(false); }); }); diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js deleted file mode 100644 index cc425cc7301..00000000000 --- a/spec/frontend/diffs/components/edit_button_spec.js +++ /dev/null @@ -1,140 +0,0 @@ -import { shallowMount, mount } from '@vue/test-utils'; -import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import EditButton from '~/diffs/components/edit_button.vue'; - -jest.mock('lodash/uniqueId', () => (str = '') => `${str}fake`); - -const TOOLTIP_ID = 'edit_button_tooltip_fake'; -const EDIT_ITEM = { - href: 'test-path', - text: 'Edit in single-file editor', -}; -const IDE_EDIT_ITEM = { - href: 'ide-test-path', - text: 'Edit in Web IDE', -}; - -describe('EditButton', () => { - let wrapper; - - const createComponent = (props = {}, mountFn = shallowMount) => { - wrapper = mountFn(EditButton, { - propsData: { - editPath: EDIT_ITEM.href, - ideEditPath: IDE_EDIT_ITEM.href, - canCurrentUserFork: false, - ...props, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; - const findDropdown = () => wrapper.find(GlDeprecatedDropdown); - const parseDropdownItems = () => - wrapper.findAll(GlDeprecatedDropdownItem).wrappers.map(x => ({ - text: x.text(), - href: x.attributes('href'), - })); - const triggerShow = () => { - const event = new Event(''); - jest.spyOn(event, 'preventDefault'); - - findDropdown().vm.$emit('show', event); - - return event; - }; - - it.each` - props | expectedItems - ${{}} | ${[EDIT_ITEM, IDE_EDIT_ITEM]} - ${{ editPath: '' }} | ${[IDE_EDIT_ITEM]} - ${{ ideEditPath: '' }} | ${[EDIT_ITEM]} - `('should render items with=$props', ({ props, expectedItems }) => { - createComponent(props); - - expect(parseDropdownItems()).toEqual(expectedItems); - }); - - describe('with default', () => { - beforeEach(() => { - createComponent({}, mount); - }); - - it('does not have tooltip', () => { - expect(getTooltip()).toEqual({ id: TOOLTIP_ID, title: 'Edit file in...' }); - }); - - it('shows pencil dropdown', () => { - expect(wrapper.find(GlIcon).props('name')).toBe('pencil'); - expect(wrapper.find('.gl-dropdown-caret').exists()).toBe(true); - }); - - describe.each` - event | expectedEmit | expectedRootEmit - ${'show'} | ${'open'} | ${[['bv::hide::tooltip', TOOLTIP_ID]]} - ${'hide'} | ${'close'} | ${[]} - `('when dropdown emits $event', ({ event, expectedEmit, expectedRootEmit }) => { - let rootEmitSpy; - - beforeEach(() => { - rootEmitSpy = jest.spyOn(wrapper.vm.$root, '$emit'); - - findDropdown().vm.$emit(event); - }); - - it(`emits ${expectedEmit}`, () => { - expect(wrapper.emitted(expectedEmit)).toEqual([[]]); - }); - - it(`emits root = ${JSON.stringify(expectedRootEmit)}`, () => { - expect(rootEmitSpy.mock.calls).toEqual(expectedRootEmit); - }); - }); - }); - - describe('with cant modify blob and can fork', () => { - beforeEach(() => { - createComponent({ - canModifyBlob: false, - canCurrentUserFork: true, - }); - }); - - it('when try to open, emits showForkMessage', () => { - expect(wrapper.emitted('showForkMessage')).toBeUndefined(); - - const event = triggerShow(); - - expect(wrapper.emitted('showForkMessage')).toEqual([[]]); - expect(event.preventDefault).toHaveBeenCalled(); - expect(wrapper.emitted('open')).toBeUndefined(); - }); - }); - - describe('with editPath is falsey', () => { - beforeEach(() => { - createComponent({ - editPath: '', - }); - }); - - it('should disable dropdown', () => { - expect(findDropdown().attributes('disabled')).toBe('true'); - }); - - it('should have tooltip', () => { - expect(getTooltip()).toEqual({ - id: TOOLTIP_ID, - title: "Can't edit as source branch was deleted", - }); - }); - }); -}); diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js index dac5be2d656..4f1f3565f85 100644 --- a/spec/frontend/diffs/store/getters_spec.js +++ b/spec/frontend/diffs/store/getters_spec.js @@ -139,50 +139,74 @@ describe('Diffs Module Getters', () => { describe('diffHasExpandedDiscussions', () => { it('returns true when one of the discussions is expanded', () => { - discussionMock1.expanded = false; + const diffFile = { + parallel_diff_lines: [], + highlighted_diff_lines: [ + { + discussions: [discussionMock, discussionMock], + discussionsExpanded: true, + }, + ], + }; - expect( - getters.diffHasExpandedDiscussions(localState, { - getDiffFileDiscussions: () => [discussionMock, discussionMock], - })(diffFileMock), - ).toEqual(true); + expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(true); }); it('returns false when there are no discussions', () => { - expect( - getters.diffHasExpandedDiscussions(localState, { getDiffFileDiscussions: () => [] })( - diffFileMock, - ), - ).toEqual(false); + const diffFile = { + parallel_diff_lines: [], + highlighted_diff_lines: [ + { + discussions: [], + discussionsExpanded: true, + }, + ], + }; + expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false); }); it('returns false when no discussion is expanded', () => { - discussionMock.expanded = false; - discussionMock1.expanded = false; + const diffFile = { + parallel_diff_lines: [], + highlighted_diff_lines: [ + { + discussions: [discussionMock, discussionMock], + discussionsExpanded: false, + }, + ], + }; - expect( - getters.diffHasExpandedDiscussions(localState, { - getDiffFileDiscussions: () => [discussionMock, discussionMock1], - })(diffFileMock), - ).toEqual(false); + expect(getters.diffHasExpandedDiscussions(localState)(diffFile)).toEqual(false); }); }); describe('diffHasDiscussions', () => { it('returns true when getDiffFileDiscussions returns discussions', () => { - expect( - getters.diffHasDiscussions(localState, { - getDiffFileDiscussions: () => [discussionMock], - })(diffFileMock), - ).toEqual(true); + const diffFile = { + parallel_diff_lines: [], + highlighted_diff_lines: [ + { + discussions: [discussionMock, discussionMock], + discussionsExpanded: false, + }, + ], + }; + + expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(true); }); it('returns false when getDiffFileDiscussions returns no discussions', () => { - expect( - getters.diffHasDiscussions(localState, { - getDiffFileDiscussions: () => [], - })(diffFileMock), - ).toEqual(false); + const diffFile = { + parallel_diff_lines: [], + highlighted_diff_lines: [ + { + discussions: [], + discussionsExpanded: false, + }, + ], + }; + + expect(getters.diffHasDiscussions(localState)(diffFile)).toEqual(false); }); }); diff --git a/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js index 557ef48e908..766a27015bb 100644 --- a/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js +++ b/spec/frontend/issue_show/components/incidents/highlight_bar_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlLink } from '@gitlab/ui'; import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue'; import { formatDate } from '~/lib/utils/datetime_utility'; @@ -21,9 +21,6 @@ describe('Highlight Bar', () => { propsData: { alert, }, - stubs: { - GlSprintf, - }, }); }; @@ -44,7 +41,7 @@ describe('Highlight Bar', () => { expect(findLink().exists()).toBe(true); expect(findLink().attributes('href')).toBe(alert.detailsUrl); expect(findLink().attributes('title')).toBe(alert.title); - expect(findLink().text()).toBe(`Alert #${alert.iid}`); + expect(findLink().text()).toBe(`#${alert.iid}`); }); it('renders formatted start time of the alert', () => { diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js index affd6c1d1d2..d82590c7e9e 100644 --- a/spec/frontend/notes/components/discussion_counter_spec.js +++ b/spec/frontend/notes/components/discussion_counter_spec.js @@ -1,6 +1,6 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import notesModule from '~/notes/stores/modules'; import DiscussionCounter from '~/notes/components/discussion_counter.vue'; import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data'; @@ -9,6 +9,7 @@ import * as types from '~/notes/stores/mutation_types'; describe('DiscussionCounter component', () => { let store; let wrapper; + let setExpandDiscussionsFn; const localVue = createLocalVue(); localVue.use(Vuex); @@ -16,6 +17,7 @@ describe('DiscussionCounter component', () => { beforeEach(() => { window.mrTabs = {}; const { state, getters, mutations, actions } = notesModule(); + setExpandDiscussionsFn = jest.fn().mockImplementation(actions.setExpandDiscussions); store = new Vuex.Store({ state: { @@ -24,7 +26,10 @@ describe('DiscussionCounter component', () => { }, getters, mutations, - actions, + actions: { + ...actions, + setExpandDiscussions: setExpandDiscussionsFn, + }, }); store.dispatch('setNoteableData', { ...noteableDataMock, @@ -84,7 +89,7 @@ describe('DiscussionCounter component', () => { wrapper = shallowMount(DiscussionCounter, { store, localVue }); expect(wrapper.find(`.is-active`).exists()).toBe(isActive); - expect(wrapper.findAll('[role="group"').length).toBe(groupLength); + expect(wrapper.findAll(GlButton)).toHaveLength(groupLength); }); }); @@ -103,23 +108,22 @@ describe('DiscussionCounter component', () => { it('calls button handler when clicked', () => { updateStoreWithExpanded(true); - wrapper.setMethods({ handleExpandDiscussions: jest.fn() }); - toggleAllButton.trigger('click'); + toggleAllButton.vm.$emit('click'); - expect(wrapper.vm.handleExpandDiscussions).toHaveBeenCalledTimes(1); + expect(setExpandDiscussionsFn).toHaveBeenCalledTimes(1); }); it('collapses all discussions if expanded', () => { updateStoreWithExpanded(true); expect(wrapper.vm.allExpanded).toBe(true); - expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-up'); + expect(toggleAllButton.props('icon')).toBe('angle-up'); - toggleAllButton.trigger('click'); + toggleAllButton.vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.allExpanded).toBe(false); - expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-down'); + expect(toggleAllButton.props('icon')).toBe('angle-down'); }); }); @@ -127,13 +131,13 @@ describe('DiscussionCounter component', () => { updateStoreWithExpanded(false); expect(wrapper.vm.allExpanded).toBe(false); - expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-down'); + expect(toggleAllButton.props('icon')).toBe('angle-down'); - toggleAllButton.trigger('click'); + toggleAllButton.vm.$emit('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.allExpanded).toBe(true); - expect(toggleAllButton.find(GlIcon).props().name).toBe('angle-up'); + expect(toggleAllButton.props('icon')).toBe('angle-up'); }); }); }); diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js index b6abb9f389a..c1fad8cebe6 100644 --- a/spec/frontend/snippets/components/edit_spec.js +++ b/spec/frontend/snippets/components/edit_spec.js @@ -148,17 +148,17 @@ describe('Snippet Edit app', () => { // Ideally we wouldn't call this method directly, but we don't have a way to trigger // apollo responses yet. - const loadSnippet = (...edges) => { - if (edges.length) { + const loadSnippet = (...nodes) => { + if (nodes.length) { wrapper.setData({ - snippet: edges[0], + snippet: nodes[0], }); } wrapper.vm.onSnippetFetch({ data: { snippets: { - edges, + nodes, }, }, }); diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index 9c4b2734a3f..1ccecd7b5ba 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -140,10 +140,10 @@ describe('Blob Embeddable', () => { async ({ snippetBlobs, currentBlob, expectedContent }) => { const apolloData = { snippets: { - edges: [ + nodes: [ { - node: { - blobs: snippetBlobs, + blobs: { + nodes: snippetBlobs, }, }, ], diff --git a/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb b/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb new file mode 100644 index 00000000000..fdbd87c32be --- /dev/null +++ b/spec/graphql/resolvers/snippets/blobs_resolver_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Snippets::BlobsResolver do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:current_user) { create(:user) } + let_it_be(:snippet) { create(:personal_snippet, :private, :repository, author: current_user) } + + context 'when user is not authorized' do + let(:other_user) { create(:user) } + + it 'raises an error' do + expect do + resolve_blobs(snippet, user: other_user) + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when using no filter' do + it 'returns all snippet blobs' do + expect(resolve_blobs(snippet).map(&:path)).to contain_exactly(*snippet.list_files) + end + end + + context 'when using filters' do + context 'when paths is a single string' do + it 'returns an array of files' do + path = 'CHANGELOG' + + expect(resolve_blobs(snippet, args: { paths: path }).first.path).to eq(path) + end + end + + context 'when paths is an array of string' do + it 'returns an array of files' do + paths = ['CHANGELOG', 'README.md'] + + expect(resolve_blobs(snippet, args: { paths: paths }).map(&:path)).to contain_exactly(*paths) + end + end + end + end + + def resolve_blobs(snippet, user: current_user, args: {}) + resolve(described_class, args: args, ctx: { current_user: user }, obj: snippet) + end +end diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb index 86af69f1294..2ea215450cb 100644 --- a/spec/graphql/types/snippet_type_spec.rb +++ b/spec/graphql/types/snippet_type_spec.rb @@ -16,6 +16,15 @@ RSpec.describe GitlabSchema.types['Snippet'] do expect(described_class).to have_graphql_fields(*expected_fields) end + describe 'blobs field' do + subject { described_class.fields['blobs'] } + + it 'returns blobs' do + is_expected.to have_graphql_type(Types::Snippets::BlobType.connection_type) + is_expected.to have_graphql_resolver(Resolvers::Snippets::BlobsResolver) + end + end + context 'when restricted visibility level is set to public' do let_it_be(:snippet) { create(:personal_snippet, :repository, :public, author: user) } @@ -142,9 +151,30 @@ RSpec.describe GitlabSchema.types['Snippet'] do describe '#blobs' do let_it_be(:snippet) { create(:personal_snippet, :public, author: user) } - let(:query_blobs) { subject.dig('data', 'snippets', 'edges')[0]['node']['blobs'] } + let(:query_blobs) { subject.dig('data', 'snippets', 'edges')[0].dig('node', 'blobs', 'edges') } + let(:paths) { [] } + let(:query) do + %( + { + snippets { + edges { + node { + blobs(paths: #{paths}) { + edges { + node { + name + path + } + } + } + } + } + } + } + ) + end - subject { GitlabSchema.execute(snippet_query_for(field: 'blobs'), context: { current_user: user }).as_json } + subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } shared_examples 'an array' do it 'returns an array of snippet blobs' do @@ -158,8 +188,8 @@ RSpec.describe GitlabSchema.types['Snippet'] do it_behaves_like 'an array' it 'contains the first blob from the snippet' do - expect(query_blobs.first['name']).to eq blob.name - expect(query_blobs.first['path']).to eq blob.path + expect(query_blobs.first['node']['name']).to eq blob.name + expect(query_blobs.first['node']['path']).to eq blob.path end end @@ -170,10 +200,22 @@ RSpec.describe GitlabSchema.types['Snippet'] do it_behaves_like 'an array' it 'contains all the blobs from the repository' do - resulting_blobs_names = query_blobs.map { |b| b['name'] } + resulting_blobs_names = query_blobs.map { |b| b['node']['name'] } expect(resulting_blobs_names).to match_array(blobs.map(&:name)) end + + context 'when specific path is set' do + let(:paths) { ['CHANGELOG'] } + + it_behaves_like 'an array' + + it 'returns specific files' do + resulting_blobs_names = query_blobs.map { |b| b['node']['name'] } + + expect(resulting_blobs_names).to match(paths) + end + end end end diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb index ccf96bcbad6..6d06fc3618d 100644 --- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb +++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb @@ -24,6 +24,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do shared_examples 'executing redirect' do it 'redirects to package registry' do + expect(helper).to receive(:track_event).with('npm_request_forward').once expect(helper).to receive(:registry_url).once expect(helper).to receive(:redirect).once expect(helper).to receive(:fallback).never @@ -63,6 +64,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do let(:package_type) { pkg_type } it 'raises an error' do + allow(helper).to receive(:track_event) expect { subject }.to raise_error(ArgumentError, "Can't build registry_url for package_type #{package_type}") end end diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index 63f948e8dd4..35df2b7f241 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -17,23 +17,33 @@ RSpec.describe Backup::Repositories do end describe '#dump' do - before do - allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys) - end - - let_it_be(:projects) { create_list(:project, 5, :repository, :wiki_repo) } + let_it_be(:projects) { create_list(:project, 5, :repository) } - let(:storage_keys) { %w[default test_second_storage] } + RSpec.shared_examples 'creates repository bundles' do + specify :aggregate_failures do + # Add data to the wiki repository, so it will be included in the dump. + create(:wiki_page, container: project) - context 'no concurrency' do - it 'creates repository bundle' do subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - projects.each do |project| - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) - end + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) + expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.wiki' + '.bundle')) end + end + + context 'hashed storage' do + let_it_be(:project) { create(:project, :repository) } + + it_behaves_like 'creates repository bundles' + end + + context 'legacy storage' do + let_it_be(:project) { create(:project, :repository, :legacy_storage) } + + it_behaves_like 'creates repository bundles' + end + context 'no concurrency' do it 'creates the expected number of threads' do expect(Thread).not_to receive(:new) @@ -63,26 +73,22 @@ RSpec.describe Backup::Repositories do subject.dump(max_concurrency: 1, max_storage_concurrency: 1) end.count - create_list(:project, 2, :repository, :wiki_repo) + create_list(:project, 2, :repository) expect do subject.dump(max_concurrency: 1, max_storage_concurrency: 1) end.not_to exceed_query_limit(control_count) end - - context 'legacy storage' do - let_it_be(:project) { create(:project, :repository, :legacy_storage, :wiki_repo) } - - it 'creates repository bundle' do - subject.dump(max_concurrency: 1, max_storage_concurrency: 1) - - expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project.disk_path + '.bundle')) - end - end end [4, 10].each do |max_storage_concurrency| context "max_storage_concurrency #{max_storage_concurrency}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241701' do + let(:storage_keys) { %w[default test_second_storage] } + + before do + allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(storage_keys) + end + it 'creates the expected number of threads' do expect(Thread).to receive(:new) .exactly(storage_keys.length * (max_storage_concurrency + 1)).times @@ -135,7 +141,7 @@ RSpec.describe Backup::Repositories do subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) end.count - create_list(:project, 2, :repository, :wiki_repo) + create_list(:project, 2, :repository) expect do subject.dump(max_concurrency: 1, max_storage_concurrency: max_storage_concurrency) @@ -146,7 +152,25 @@ RSpec.describe Backup::Repositories do end describe '#restore' do - let_it_be(:project) { create(:project, :wiki_repo) } + let_it_be(:project) { create(:project) } + + it 'restores repositories from bundles', :aggregate_failures do + next_path_to_bundle = [ + Rails.root.join('spec/fixtures/lib/backup/project_repo.bundle'), + Rails.root.join('spec/fixtures/lib/backup/wiki_repo.bundle') + ].to_enum + + allow_next_instance_of(described_class::BackupRestore) do |backup_restore| + allow(backup_restore).to receive(:path_to_bundle).and_return(next_path_to_bundle.next) + end + + subject.restore + + collect_commit_shas = -> (repo) { repo.commits('master', limit: 10).map(&:sha) } + + expect(collect_commit_shas.call(project.repository)).to eq(['393a7d860a5a4c3cc736d7eb00604e3472bb95ec']) + expect(collect_commit_shas.call(project.wiki.repository)).to eq(['c74b9948d0088d703ee1fafeddd9ed9add2901ea']) + end describe 'command failure' do before do diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb index db76eb9538b..55b4f7479b8 100644 --- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb +++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb @@ -74,14 +74,6 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do } ) end - - it 'does not save a json file if feature is disabled' do - stub_feature_flags(export_lfs_objects_projects: false) - - saver.save - - expect(File.exist?(lfs_json_file)).to eq(false) - end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 283d945157b..3cbffa7cb86 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Issue do describe 'versions.most_recent' do it 'returns the most recent version' do - issue = create(:issue) + issue = create(:issue, project: reusable_project) create_list(:design_version, 2, issue: issue) last_version = create(:design_version, issue: issue) @@ -79,19 +79,19 @@ RSpec.describe Issue do end end - subject { create(:issue) } + subject { create(:issue, project: reusable_project) } describe 'callbacks' do describe '#ensure_metrics' do it 'creates metrics after saving' do - issue = create(:issue) + issue = create(:issue, project: reusable_project) expect(issue.metrics).to be_persisted expect(Issue::Metrics.count).to eq(1) end it 'does not create duplicate metrics for an issue' do - issue = create(:issue) + issue = create(:issue, project: reusable_project) issue.close! @@ -102,7 +102,7 @@ RSpec.describe Issue do it 'records current metrics' do expect_any_instance_of(Issue::Metrics).to receive(:record!) - create(:issue) + create(:issue, project: reusable_project) end end end @@ -111,8 +111,8 @@ RSpec.describe Issue do subject { described_class.with_alert_management_alerts } it 'gets only issues with alerts' do - alert = create(:alert_management_alert, issue: create(:issue)) - issue = create(:issue) + alert = create(:alert_management_alert, project: reusable_project, issue: create(:issue, project: reusable_project)) + issue = create(:issue, project: reusable_project) expect(subject).to contain_exactly(alert.issue) expect(subject).not_to include(issue) @@ -130,10 +130,9 @@ RSpec.describe Issue do end describe '.with_issue_type' do - let_it_be(:project) { create(:project) } - let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:incident) { create(:incident, project: project) } - let_it_be(:test_case) { create(:quality_test_case, project: project) } + let_it_be(:issue) { create(:issue, project: reusable_project) } + let_it_be(:incident) { create(:incident, project: reusable_project) } + let_it_be(:test_case) { create(:quality_test_case, project: reusable_project) } it 'gives issues with the given issue type' do expect(described_class.with_issue_type('issue')) @@ -195,7 +194,7 @@ RSpec.describe Issue do end describe '#close' do - subject(:issue) { create(:issue, state: 'opened') } + subject(:issue) { create(:issue, project: reusable_project, state: 'opened') } it 'sets closed_at to Time.current when an issue is closed' do expect { issue.close }.to change { issue.closed_at }.from(nil) @@ -210,7 +209,7 @@ RSpec.describe Issue do end describe '#reopen' do - let(:issue) { create(:issue, state: 'closed', closed_at: Time.current, closed_by: user) } + let(:issue) { create(:issue, project: reusable_project, state: 'closed', closed_at: Time.current, closed_by: user) } it 'sets closed_at to nil when an issue is reopend' do expect { issue.reopen }.to change { issue.closed_at }.to(nil) @@ -293,7 +292,7 @@ RSpec.describe Issue do end describe '#assignee_or_author?' do - let(:issue) { create(:issue) } + let(:issue) { create(:issue, project: reusable_project) } it 'returns true for a user that is assigned to an issue' do issue.assignees << user @@ -313,22 +312,21 @@ RSpec.describe Issue do end describe '#related_issues' do - let(:user) { create(:user) } - let(:authorized_project) { create(:project) } - let(:authorized_project2) { create(:project) } - let(:unauthorized_project) { create(:project) } + let_it_be(:authorized_project) { create(:project) } + let_it_be(:authorized_project2) { create(:project) } + let_it_be(:unauthorized_project) { create(:project) } - let(:authorized_issue_a) { create(:issue, project: authorized_project) } - let(:authorized_issue_b) { create(:issue, project: authorized_project) } - let(:authorized_issue_c) { create(:issue, project: authorized_project2) } + let_it_be(:authorized_issue_a) { create(:issue, project: authorized_project) } + let_it_be(:authorized_issue_b) { create(:issue, project: authorized_project) } + let_it_be(:authorized_issue_c) { create(:issue, project: authorized_project2) } - let(:unauthorized_issue) { create(:issue, project: unauthorized_project) } + let_it_be(:unauthorized_issue) { create(:issue, project: unauthorized_project) } - let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) } - let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) } - let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) } + let_it_be(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) } + let_it_be(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) } + let_it_be(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) } - before do + before_all do authorized_project.add_developer(user) authorized_project2.add_developer(user) end @@ -366,17 +364,16 @@ RSpec.describe Issue do end context 'user is reporter in project issue belongs to' do - let(:project) { create(:project) } - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: reusable_project) } - before do - project.add_reporter(user) + before_all do + reusable_project.add_reporter(user) end it { is_expected.to eq true } context 'issue not persisted' do - let(:issue) { build(:issue, project: project) } + let(:issue) { build(:issue, project: reusable_project) } it { is_expected.to eq false } end @@ -384,7 +381,7 @@ RSpec.describe Issue do context 'checking destination project also' do subject { issue.can_move?(user, to_project) } - let(:to_project) { create(:project) } + let_it_be(:to_project) { create(:project) } context 'destination project allowed' do before do @@ -420,7 +417,7 @@ RSpec.describe Issue do end describe '#duplicated?' do - let(:issue) { create(:issue) } + let(:issue) { create(:issue, project: reusable_project) } subject { issue.duplicated? } @@ -429,7 +426,7 @@ RSpec.describe Issue do end context 'issue already duplicated' do - let(:duplicated_to_issue) { create(:issue) } + let(:duplicated_to_issue) { create(:issue, project: reusable_project) } let(:issue) { create(:issue, duplicated_to: duplicated_to_issue) } it { is_expected.to eq true } @@ -440,13 +437,13 @@ RSpec.describe Issue do subject { issue.from_service_desk? } context 'when issue author is support bot' do - let(:issue) { create(:issue, author: ::User.support_bot) } + let(:issue) { create(:issue, project: reusable_project, author: ::User.support_bot) } it { is_expected.to be_truthy } end context 'when issue author is not support bot' do - let(:issue) { create(:issue) } + let(:issue) { create(:issue, project: reusable_project) } it { is_expected.to be_falsey } end @@ -495,7 +492,7 @@ RSpec.describe Issue do end describe '#has_related_branch?' do - let(:issue) { create(:issue, title: "Blue Bell Knoll") } + let(:issue) { create(:issue, project: reusable_project, title: "Blue Bell Knoll") } subject { issue.has_related_branch? } @@ -528,7 +525,7 @@ RSpec.describe Issue do end describe "#to_branch_name" do - let(:issue) { create(:issue, title: 'testing-issue') } + let_it_be(:issue) { create(:issue, project: reusable_project, title: 'testing-issue') } it 'starts with the issue iid' do expect(issue.to_branch_name).to match(/\A#{issue.iid}-[A-Za-z\-]+\z/) @@ -539,12 +536,12 @@ RSpec.describe Issue do end it "does not contain the issue title if confidential" do - issue = create(:issue, title: 'testing-issue', confidential: true) + issue = create(:issue, project: reusable_project, title: 'testing-issue', confidential: true) expect(issue.to_branch_name).to match(/confidential-issue\z/) end context 'issue title longer than 100 characters' do - let(:issue) { create(:issue, iid: 999, title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit Mauris sit amet ipsum id lacus custom fringilla convallis') } + let_it_be(:issue) { create(:issue, project: reusable_project, iid: 999, title: 'Lorem ipsum dolor sit amet consectetur adipiscing elit Mauris sit amet ipsum id lacus custom fringilla convallis') } it "truncates branch name to at most 100 characters" do expect(issue.to_branch_name.length).to be <= 100 @@ -581,15 +578,14 @@ RSpec.describe Issue do describe '#participants' do context 'using a public project' do - let(:project) { create(:project, :public) } - let(:issue) { create(:issue, project: project) } + let_it_be(:issue) { create(:issue, project: reusable_project) } let!(:note1) do - create(:note_on_issue, noteable: issue, project: project, note: 'a') + create(:note_on_issue, noteable: issue, project: reusable_project, note: 'a') end let!(:note2) do - create(:note_on_issue, noteable: issue, project: project, note: 'b') + create(:note_on_issue, noteable: issue, project: reusable_project, note: 'b') end it 'includes the issue author' do @@ -604,8 +600,8 @@ RSpec.describe Issue do context 'using a private project' do it 'does not include mentioned users that do not have access to the project' do project = create(:project) - user = create(:user) issue = create(:issue, project: project) + user = create(:user) create(:note_on_issue, noteable: issue, @@ -621,10 +617,9 @@ RSpec.describe Issue do it 'updates when assignees change' do user1 = create(:user) user2 = create(:user) - project = create(:project) - issue = create(:issue, assignees: [user1], project: project) - project.add_developer(user1) - project.add_developer(user2) + issue = create(:issue, assignees: [user1], project: reusable_project) + reusable_project.add_developer(user1) + reusable_project.add_developer(user2) expect(user1.assigned_open_issues_count).to eq(1) expect(user2.assigned_open_issues_count).to eq(0) @@ -638,9 +633,8 @@ RSpec.describe Issue do end describe '#visible_to_user?' do - let(:project) { build(:project) } + let(:project) { reusable_project } let(:issue) { build(:issue, project: project) } - let(:user) { create(:user) } subject { issue.visible_to_user?(user) } @@ -661,6 +655,10 @@ RSpec.describe Issue do context 'without a user' do let(:user) { nil } + before do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::PUBLIC) + end + it 'returns true when the issue is publicly visible' do expect(issue).to receive(:publicly_visible?).and_return(true) @@ -995,7 +993,8 @@ RSpec.describe Issue do with_them do it 'checks for spam on issues that can be seen anonymously' do - project = create(:project, visibility_level: visibility_level) + project = reusable_project + project.update(visibility_level: visibility_level) issue = create(:issue, project: project, confidential: confidential, description: 'original description') issue.assign_attributes(new_attributes) @@ -1016,8 +1015,8 @@ RSpec.describe Issue do describe '.public_only' do it 'only returns public issues' do - public_issue = create(:issue) - create(:issue, confidential: true) + public_issue = create(:issue, project: reusable_project) + create(:issue, project: reusable_project, confidential: true) expect(described_class.public_only).to eq([public_issue]) end @@ -1025,15 +1024,15 @@ RSpec.describe Issue do describe '.confidential_only' do it 'only returns confidential_only issues' do - create(:issue) - confidential_issue = create(:issue, confidential: true) + create(:issue, project: reusable_project) + confidential_issue = create(:issue, project: reusable_project, confidential: true) expect(described_class.confidential_only).to eq([confidential_issue]) end end describe '.by_project_id_and_iid' do - let_it_be(:issue_a) { create(:issue) } + let_it_be(:issue_a) { create(:issue, project: reusable_project) } let_it_be(:issue_b) { create(:issue, iid: issue_a.iid) } let_it_be(:issue_c) { create(:issue, project: issue_a.project) } let_it_be(:issue_d) { create(:issue, project: issue_a.project) } @@ -1050,8 +1049,8 @@ RSpec.describe Issue do describe '.service_desk' do it 'returns the service desk issue' do - service_desk_issue = create(:issue, author: ::User.support_bot) - regular_issue = create(:issue) + service_desk_issue = create(:issue, project: reusable_project, author: ::User.support_bot) + regular_issue = create(:issue, project: reusable_project) expect(described_class.service_desk).to include(service_desk_issue) expect(described_class.service_desk).not_to include(regular_issue) @@ -1064,7 +1063,7 @@ RSpec.describe Issue do describe "#labels_hook_attrs" do let(:label) { create(:label) } - let(:issue) { create(:labeled_issue, labels: [label]) } + let(:issue) { create(:labeled_issue, project: reusable_project, labels: [label]) } it "returns a list of label hook attributes" do expect(issue.labels_hook_attrs).to eq([label.hook_attrs]) @@ -1073,7 +1072,7 @@ RSpec.describe Issue do context "relative positioning" do it_behaves_like "a class that supports relative positioning" do - let_it_be(:project) { create(:project) } + let_it_be(:project) { reusable_project } let(:factory) { :issue } let(:default_params) { { project: project } } end @@ -1083,7 +1082,7 @@ RSpec.describe Issue do describe "#previous_updated_at" do let_it_be(:updated_at) { Time.zone.local(2012, 01, 06) } - let_it_be(:issue) { create(:issue, updated_at: updated_at) } + let_it_be(:issue) { create(:issue, project: reusable_project, updated_at: updated_at) } it 'returns updated_at value if updated_at did not change at all' do allow(issue).to receive(:previous_changes).and_return({}) @@ -1121,7 +1120,7 @@ RSpec.describe Issue do end describe 'current designs' do - let(:issue) { create(:issue) } + let(:issue) { create(:issue, project: reusable_project) } subject { issue.designs.current } diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb index 4cac90786eb..9b6cec99ddb 100644 --- a/spec/models/wiki_directory_spec.rb +++ b/spec/models/wiki_directory_spec.rb @@ -3,43 +3,97 @@ require 'spec_helper' RSpec.describe WikiDirectory do - describe 'validations' do - subject { build(:wiki_directory) } + subject(:directory) { build(:wiki_directory) } + describe 'validations' do it { is_expected.to validate_presence_of(:slug) } end + describe '.group_pages' do + let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') } + let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') } + let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') } + let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') } + let_it_be(:child2) { build(:wiki_page, title: 'parent1/child2') } + let_it_be(:child3) { build(:wiki_page, title: 'parent2/child3') } + let_it_be(:grandchild1) { build(:wiki_page, title: 'parent1/subparent/grandchild1') } + let_it_be(:grandchild2) { build(:wiki_page, title: 'parent1/subparent/grandchild2') } + + it 'returns a nested array of entries' do + entries = described_class.group_pages( + [toplevel1, toplevel2, toplevel3, child1, child2, child3, grandchild1, grandchild2].sort_by(&:title) + ) + + expect(entries).to match([ + toplevel1, + a_kind_of(WikiDirectory).and( + having_attributes( + slug: 'parent1', entries: [ + child1, + child2, + a_kind_of(WikiDirectory).and( + having_attributes( + slug: 'parent1/subparent', + entries: [grandchild1, grandchild2] + ) + ) + ] + ) + ), + a_kind_of(WikiDirectory).and( + having_attributes( + slug: 'parent2', + entries: [child3] + ) + ), + toplevel2, + toplevel3 + ]) + end + end + describe '#initialize' do - context 'when there are pages' do - let(:pages) { [build(:wiki_page)] } - let(:directory) { described_class.new('/path_up_to/dir', pages) } + context 'when there are entries' do + let(:entries) { [build(:wiki_page)] } + let(:directory) { described_class.new('/path_up_to/dir', entries) } it 'sets the slug attribute' do expect(directory.slug).to eq('/path_up_to/dir') end - it 'sets the pages attribute' do - expect(directory.pages).to eq(pages) + it 'sets the entries attribute' do + expect(directory.entries).to eq(entries) end end - context 'when there are no pages' do + context 'when there are no entries' do let(:directory) { described_class.new('/path_up_to/dir') } it 'sets the slug attribute' do expect(directory.slug).to eq('/path_up_to/dir') end - it 'sets the pages attribute to an empty array' do - expect(directory.pages).to eq([]) + it 'sets the entries attribute to an empty array' do + expect(directory.entries).to eq([]) end end end + describe '#title' do + it 'returns the basename of the directory, with hyphens replaced by spaces' do + directory.slug = 'parent' + expect(directory.title).to eq('parent') + + directory.slug = 'parent/child' + expect(directory.title).to eq('child') + + directory.slug = 'parent/child-foo' + expect(directory.title).to eq('child foo') + end + end + describe '#to_partial_path' do it 'returns the relative path to the partial to be used' do - directory = build(:wiki_directory) - expect(directory.to_partial_path).to eq('../shared/wikis/wiki_directory') end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index aa8b9ce58b9..1dbf0983f19 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -23,89 +23,6 @@ RSpec.describe WikiPage do stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => thing) end - describe '.group_by_directory' do - context 'when there are no pages' do - it 'returns an empty array' do - expect(described_class.group_by_directory(nil)).to eq([]) - expect(described_class.group_by_directory([])).to eq([]) - end - end - - context 'when there are pages' do - before do - wiki.create_page('dir_1/dir_1_1/page_3', 'content') - wiki.create_page('page_1', 'content') - wiki.create_page('dir_1/page_2', 'content') - wiki.create_page('dir_2', 'page with dir name') - wiki.create_page('dir_2/page_5', 'content') - wiki.create_page('page_6', 'content') - wiki.create_page('dir_2/page_4', 'content') - end - - let(:page_1) { wiki.find_page('page_1') } - let(:page_6) { wiki.find_page('page_6') } - let(:page_dir_2) { wiki.find_page('dir_2') } - - let(:dir_1) do - WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')]) - end - - let(:dir_1_1) do - WikiDirectory.new('dir_1/dir_1_1', [wiki.find_page('dir_1/dir_1_1/page_3')]) - end - - let(:dir_2) do - pages = [wiki.find_page('dir_2/page_5'), - wiki.find_page('dir_2/page_4')] - WikiDirectory.new('dir_2', pages) - end - - describe "#list_pages" do - context 'sort by title' do - let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) } - let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] } - - it 'returns an array with pages and directories' do - grouped_entries.each_with_index do |page_or_dir, i| - expected_page_or_dir = expected_grouped_entries[i] - expected_slugs = get_slugs(expected_page_or_dir) - slugs = get_slugs(page_or_dir) - - expect(slugs).to match_array(expected_slugs) - end - end - end - - context 'sort by created_at' do - let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) } - let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] } - - it 'returns an array with pages and directories' do - grouped_entries.each_with_index do |page_or_dir, i| - expected_page_or_dir = expected_grouped_entries[i] - expected_slugs = get_slugs(expected_page_or_dir) - slugs = get_slugs(page_or_dir) - - expect(slugs).to match_array(expected_slugs) - end - end - end - - it 'returns an array with retained order with directories at the top' do - expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6'] - - grouped_entries = described_class.group_by_directory(wiki.list_pages) - - actual_order = - grouped_entries.flat_map do |page_or_dir| - get_slugs(page_or_dir) - end - expect(actual_order).to eq(expected_order) - end - end - end - end - describe '#front_matter' do let_it_be(:project) { create(:project) } let(:container) { project } @@ -993,14 +910,4 @@ RSpec.describe WikiPage do ) end end - - private - - def get_slugs(page_or_dir) - if page_or_dir.is_a? WikiPage - [page_or_dir.slug] - else - page_or_dir.pages.present? ? page_or_dir.pages.map(&:slug) : [] - end - end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index f1e581efd44..76b77ee0de2 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe MergeRequestPresenter do - let(:resource) { create(:merge_request, source_project: project) } - let(:project) { create(:project) } - let(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + let_it_be(:resource) { create(:merge_request, source_project: project) } + let_it_be(:user) { create(:user) } describe '#ci_status' do subject { described_class.new(resource).ci_status } @@ -73,8 +73,6 @@ RSpec.describe MergeRequestPresenter do end describe '#conflict_resolution_path' do - let(:project) { create :project } - let(:user) { create :user } let(:presenter) { described_class.new(resource, current_user: user) } let(:path) { presenter.conflict_resolution_path } @@ -107,18 +105,21 @@ RSpec.describe MergeRequestPresenter do end context 'issues links' do - let(:project) { create(:project, :private, :repository, creator: user, namespace: user.namespace) } - let(:issue_a) { create(:issue, project: project) } - let(:issue_b) { create(:issue, project: project) } + let_it_be(:project) { create(:project, :private, :repository, creator: user, namespace: user.namespace) } + let_it_be(:issue_a) { create(:issue, project: project) } + let_it_be(:issue_b) { create(:issue, project: project) } - let(:resource) do + let_it_be(:resource) do create(:merge_request, source_project: project, target_project: project, description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}") end - before do + before_all do project.add_developer(user) + end + + before do allow(resource.project).to receive(:default_branch) .and_return(resource.target_branch) resource.cache_merge_request_closes_issues! diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb index 915f43fe572..d7268c79a2c 100644 --- a/spec/presenters/snippet_blob_presenter_spec.rb +++ b/spec/presenters/snippet_blob_presenter_spec.rb @@ -3,70 +3,75 @@ require 'spec_helper' RSpec.describe SnippetBlobPresenter do + let_it_be(:snippet) { create(:personal_snippet, :repository) } + + let(:branch) { snippet.default_branch } + let(:blob) { snippet.blobs.first } + describe '#rich_data' do + let(:data_endpoint_url) { "/-/snippets/#{snippet.id}/raw/#{branch}/#{file}" } + before do allow_next_instance_of(described_class) do |instance| allow(instance).to receive(:current_user).and_return(nil) end + + blob.name = File.basename(file) + blob.path = file end - subject { described_class.new(snippet.blob).rich_data } + subject { described_class.new(blob).rich_data } context 'with PersonalSnippet' do - let(:snippet) { create(:personal_snippet, :repository) } - context 'when blob is binary' do - it 'returns the HTML associated with the binary' do - allow(snippet).to receive(:blob).and_return(snippet.repository.blob_at('master', 'files/images/logo-black.png')) + let(:file) { 'files/images/logo-black.png' } + let(:blob) { blob_at(file) } + it 'returns the HTML associated with the binary' do expect(subject).to include('file-content image_file') end end context 'with markdown format' do - let(:snippet) { create(:personal_snippet, file_name: 'test.md', content: '*foo*') } + let(:file) { 'README.md' } + let(:blob) { blob_at(file) } it 'returns rich markdown content' do - expected = <<~HTML - <div class="file-content md"> - <p data-sourcepos="1:1-1:5" dir="auto"><em>foo</em></p> - </div> - HTML - - expect(subject).to eq(expected) + expect(subject).to include('file-content md') end end context 'with notebook format' do - let(:snippet) { create(:personal_snippet, file_name: 'test.ipynb') } + let(:file) { 'test.ipynb' } it 'returns rich notebook content' do - expect(subject.strip).to eq %Q(<div class="file-content" data-endpoint="/-/snippets/#{snippet.id}/raw" id="js-notebook-viewer"></div>) + expect(subject.strip).to eq %Q(<div class="file-content" data-endpoint="#{data_endpoint_url}" id="js-notebook-viewer"></div>) end end context 'with openapi format' do - let(:snippet) { create(:personal_snippet, file_name: 'openapi.yml') } + let(:file) { 'openapi.yml' } it 'returns rich openapi content' do - expect(subject).to eq %Q(<div class="file-content" data-endpoint="/-/snippets/#{snippet.id}/raw" id="js-openapi-viewer"></div>\n) + expect(subject).to eq %Q(<div class="file-content" data-endpoint="#{data_endpoint_url}" id="js-openapi-viewer"></div>\n) end end context 'with svg format' do - let(:snippet) { create(:personal_snippet, file_name: 'test.svg') } + let(:file) { 'files/images/wm.svg' } + let(:blob) { blob_at(file) } it 'returns rich svg content' do result = Nokogiri::HTML::DocumentFragment.parse(subject) image_tag = result.search('img').first - expect(image_tag.attr('src')).to include("data:#{snippet.blob.mime_type};base64") - expect(image_tag.attr('alt')).to eq('test.svg') + expect(image_tag.attr('src')).to include("data:#{blob.mime_type};base64") + expect(image_tag.attr('alt')).to eq(File.basename(file)) end end context 'with other format' do - let(:snippet) { create(:personal_snippet, file_name: 'test') } + let(:file) { 'test' } it 'does not return no rich content' do expect(subject).to be_nil @@ -76,36 +81,41 @@ RSpec.describe SnippetBlobPresenter do end describe '#plain_data' do - let(:snippet) { build(:personal_snippet) } + let(:blob) { blob_at(file) } - subject { described_class.new(snippet.blob).plain_data } + subject { described_class.new(blob).plain_data } - it 'returns nil when the snippet blob is binary' do - allow(snippet.blob).to receive(:binary?).and_return(true) + context 'when blob is binary' do + let(:file) { 'files/images/logo-black.png' } - expect(subject).to be_nil + it 'returns nil' do + expect(subject).to be_nil + end end - it 'returns plain content when snippet file is markup' do - snippet.file_name = 'test.md' - snippet.content = '*foo*' + context 'when blob is markup' do + let(:file) { 'README.md' } - expect(subject).to eq '<span id="LC1" class="line" lang="markdown"><span class="ge">*foo*</span></span>' + it 'returns plain content' do + expect(subject).to include('<span id="LC1" class="line" lang="markdown">') + end end - it 'returns highlighted syntax content' do - snippet.file_name = 'test.rb' - snippet.content = 'class Foo;end' + context 'when blob has syntax' do + let(:file) { 'files/ruby/regex.rb' } - expect(subject) - .to eq '<span id="LC1" class="line" lang="ruby"><span class="k">class</span> <span class="nc">Foo</span><span class="p">;</span><span class="k">end</span></span>' + it 'returns highlighted syntax content' do + expect(subject) + .to include '<span id="LC1" class="line" lang="ruby"><span class="k">module</span> <span class="nn">Gitlab</span>' + end end - it 'returns plain text highlighted content' do - snippet.file_name = 'test' - snippet.content = 'foo' + context 'when blob has plain data' do + let(:file) { 'LICENSE' } - expect(subject).to eq '<span id="LC1" class="line" lang="plaintext">foo</span>' + it 'returns plain text highlighted content' do + expect(subject).to include('<span id="LC1" class="line" lang="plaintext">The MIT License (MIT)</span>') + end end end @@ -179,4 +189,8 @@ RSpec.describe SnippetBlobPresenter do end end end + + def blob_at(path) + snippet.repository.blob_at(branch, path) + end end diff --git a/spec/presenters/snippet_presenter_spec.rb b/spec/presenters/snippet_presenter_spec.rb index 681564ed2b0..66c6ba8fa0e 100644 --- a/spec/presenters/snippet_presenter_spec.rb +++ b/spec/presenters/snippet_presenter_spec.rb @@ -163,25 +163,4 @@ RSpec.describe SnippetPresenter do end end end - - describe '#blobs' do - let(:snippet) { personal_snippet } - - subject { presenter.blobs } - - context 'when snippet does not have a repository' do - it 'returns an array with one SnippetBlob' do - expect(subject.size).to eq(1) - expect(subject.first).to eq(snippet.blob) - end - end - - context 'when snippet has a repository' do - let(:snippet) { create(:snippet, :repository, author: user) } - - it 'returns an array with all repository blobs' do - expect(subject).to match_array(snippet.blobs) - end - end - end end diff --git a/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb new file mode 100644 index 00000000000..42f690f53ed --- /dev/null +++ b/spec/requests/api/graphql/mutations/boards/lists/destroy_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Boards::Lists::Destroy do + include GraphqlHelpers + + let_it_be(:current_user, reload: true) { create(:user) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:board) { create(:board, project: project) } + let_it_be(:list) { create(:list, board: board) } + let(:mutation) do + variables = { + list_id: GitlabSchema.id_from_object(list).to_s + } + + graphql_mutation(:destroy_board_list, variables) + end + + subject { post_graphql_mutation(mutation, current_user: current_user) } + + def mutation_response + graphql_mutation_response(:destroy_board_list) + end + + context 'when the user does not have permission' do + it_behaves_like 'a mutation that returns a top-level access error' + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count } + end + end + + context 'when the user has permission' do + before do + project.add_maintainer(current_user) + end + + context 'when given id is not for a list' do + let_it_be(:list) { build_stubbed(:issue, project: project) } + + it 'returns an error' do + subject + + expect(graphql_errors.first['message']).to include('does not represent an instance of List') + end + end + + context 'when everything is ok' do + it 'destroys the list' do + expect { subject }.to change { List.count }.from(2).to(1) + end + + it 'returns an empty list' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('list') + expect(mutation_response['list']).to be_nil + end + end + + context 'when the list is not destroyable' do + let_it_be(:list) { create(:list, board: board, list_type: :backlog) } + + it 'does not destroy the list' do + expect { subject }.not_to change { List.count }.from(3) + end + + it 'returns an error and not nil list' do + subject + + expect(mutation_response['errors']).not_to be_empty + expect(mutation_response['list']).not_to be_nil + end + end + end +end diff --git a/spec/requests/api/graphql/mutations/snippets/create_spec.rb b/spec/requests/api/graphql/mutations/snippets/create_spec.rb index 1bb446de708..a708c3fdf1f 100644 --- a/spec/requests/api/graphql/mutations/snippets/create_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/create_spec.rb @@ -79,18 +79,20 @@ RSpec.describe 'Creating a Snippet' do end shared_examples 'creates snippet' do - it 'returns the created Snippet' do + it 'returns the created Snippet', :aggregate_failures do expect do subject end.to change { Snippet.count }.by(1) + snippet = Snippet.last + created_file_1 = snippet.repository.blob_at('HEAD', file_1[:filePath]) + created_file_2 = snippet.repository.blob_at('HEAD', file_2[:filePath]) + + expect(created_file_1.data).to match(file_1[:content]) + expect(created_file_2.data).to match(file_2[:content]) expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level) - expect(mutation_response['snippet']['blobs'][0]['plainData']).to match(file_1[:content]) - expect(mutation_response['snippet']['blobs'][0]['fileName']).to match(file_1[:file_path]) - expect(mutation_response['snippet']['blobs'][1]['plainData']).to match(file_2[:content]) - expect(mutation_response['snippet']['blobs'][1]['fileName']).to match(file_2[:file_path]) end context 'when action is invalid' do diff --git a/spec/requests/api/graphql/mutations/snippets/update_spec.rb b/spec/requests/api/graphql/mutations/snippets/update_spec.rb index 58ce74b9263..67a9869c001 100644 --- a/spec/requests/api/graphql/mutations/snippets/update_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/update_spec.rb @@ -73,7 +73,6 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_to_update.data).to eq updated_content expect(blob_to_delete).to be_nil - expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content) expect(mutation_response['snippet']['title']).to eq(updated_title) expect(mutation_response['snippet']['description']).to eq(updated_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('public') @@ -100,7 +99,6 @@ RSpec.describe 'Updating a Snippet' do aggregate_failures do expect(blob_at(updated_file).data).to eq blob_to_update.data expect(blob_at(deleted_file).data).to eq blob_to_delete.data - expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil expect(mutation_response['snippet']['title']).to eq(original_title) expect(mutation_response['snippet']['description']).to eq(original_description) expect(mutation_response['snippet']['visibilityLevel']).to eq('private') @@ -108,10 +106,6 @@ RSpec.describe 'Updating a Snippet' do end end - def blob_in_mutation_response(filename) - mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0] - end - def blob_at(filename) snippet.repository.blob_at('HEAD', filename) end diff --git a/spec/requests/api/npm_packages_spec.rb b/spec/requests/api/npm_packages_spec.rb index 108ea84b7e6..0d65c91b763 100644 --- a/spec/requests/api/npm_packages_spec.rb +++ b/spec/requests/api/npm_packages_spec.rb @@ -88,12 +88,16 @@ RSpec.describe API::NpmPackages do it_behaves_like 'returning the npm package info' context 'with unknown package' do + subject { get api("/packages/npm/unknown") } + it 'returns a redirect' do - get api("/packages/npm/unknown") + subject expect(response).to have_gitlab_http_status(:found) expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown') end + + it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward' end end diff --git a/spec/support/matchers/be_sorted.rb b/spec/support/matchers/be_sorted.rb new file mode 100644 index 00000000000..1455060fe71 --- /dev/null +++ b/spec/support/matchers/be_sorted.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Assert that this collection is sorted by argument and order +# +# By default, this checks that the collection is sorted ascending +# but you can check order by specific field and order by passing +# them, eg: +# +# ``` +# expect(collection).to be_sorted(:field, :desc) +# ``` +RSpec::Matchers.define :be_sorted do |by, order = :asc| + match do |actual| + next true unless actual.present? # emtpy collection is sorted + + actual + .then { |collection| by ? collection.sort_by(&by) : collection.sort } + .then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection } + .then { |sorted_collection| sorted_collection == actual } + end +end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index b87f7fe97e1..336a734a25f 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -164,7 +164,7 @@ RSpec.shared_examples 'wiki model' do def total_pages(entries) entries.sum do |entry| - entry.is_a?(WikiDirectory) ? entry.pages.size : 1 + entry.is_a?(WikiDirectory) ? total_pages(entry.entries) : 1 end end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 5d9694430e3..a0e1322b756 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -325,11 +325,12 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do end let!(:project_a) { create(:project, :repository) } + let!(:project_a_wiki_page) { create(:wiki_page, container: project_a) } let!(:project_b) { create(:project, :repository, repository_storage: 'test_second_storage') } let!(:b_storage_dir) { File.join(test_second_storage_dir, File.dirname(project_b.disk_path)) } - context 'no concurrency' do - it 'includes repositories in all repository storages' do + shared_examples 'includes repositories in all repository storages' do + specify :aggregate_failures do expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout tar_contents, exit_status = Gitlab::Popen.popen( @@ -337,27 +338,24 @@ RSpec.describe 'gitlab:app namespace rake task', :delete do ) expect(exit_status).to eq(0) - expect(tar_contents).to match("repositories/#{project_a.disk_path}.bundle") - expect(tar_contents).to match("repositories/#{project_b.disk_path}.bundle") + expect(tar_contents).to include( + "repositories/#{project_a.disk_path}.bundle", + "repositories/#{project_a.disk_path}.wiki.bundle", + "repositories/#{project_b.disk_path}.bundle" + ) end end + context 'no concurrency' do + it_behaves_like 'includes repositories in all repository storages' + end + context 'with concurrency' do before do stub_env('GITLAB_BACKUP_MAX_CONCURRENCY', 4) end - it 'includes repositories in all repository storages' do - expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout - - tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{backup_tar} repositories} - ) - - expect(exit_status).to eq(0) - expect(tar_contents).to match("repositories/#{project_a.disk_path}.bundle") - expect(tar_contents).to match("repositories/#{project_b.disk_path}.bundle") - end + it_behaves_like 'includes repositories in all repository storages' end end |