diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-08 15:10:06 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-08 15:10:06 +0300 |
commit | d0aeb5df3d6b06165355b023a25b79c7bd74a27d (patch) | |
tree | 7b5d3ff0f0ac5c124aa8626aeb4a0682d99a17c2 /spec | |
parent | 9ccf40d15a14e9ccf613701ba7e3d5d250961345 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
26 files changed, 727 insertions, 97 deletions
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 0b24387483b..6acbff6e745 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -54,17 +54,18 @@ RSpec.describe SendFileUpload do FileUtils.rm_f(temp_file) end - shared_examples 'handles image resize requests' do + shared_examples 'handles image resize requests' do |mount| let(:headers) { double } let(:image_requester) { build(:user) } let(:image_owner) { build(:user) } + let(:width) { mount == :pwa_icon ? 192 : 64 } let(:params) do { attachment: 'avatar.png' } end before do allow(uploader).to receive(:image_safe_for_scaling?).and_return(true) - allow(uploader).to receive(:mounted_as).and_return(:avatar) + allow(uploader).to receive(:mounted_as).and_return(mount) allow(controller).to receive(:headers).and_return(headers) # both of these are valid cases, depending on whether we are dealing with @@ -99,11 +100,11 @@ RSpec.describe SendFileUpload do context 'with valid width parameter' do it 'renders OK with workhorse command header' do expect(controller).not_to receive(:send_file) - expect(controller).to receive(:params).at_least(:once).and_return(width: '64') + expect(controller).to receive(:params).at_least(:once).and_return(width: width.to_s) expect(controller).to receive(:head).with(:ok) expect(Gitlab::Workhorse).to receive(:send_scaled_image) - .with(a_string_matching('^(/.+|https://.+)'), 64, 'image/png') + .with(a_string_matching('^(/.+|https://.+)'), width, 'image/png') .and_return([Gitlab::Workhorse::SEND_DATA_HEADER, "send-scaled-img:faux"]) expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, "send-scaled-img:faux") @@ -168,7 +169,8 @@ RSpec.describe SendFileUpload do subject end - it_behaves_like 'handles image resize requests' + it_behaves_like 'handles image resize requests', :avatar + it_behaves_like 'handles image resize requests', :pwa_icon end context 'with inline image' do @@ -273,7 +275,8 @@ RSpec.describe SendFileUpload do end end - it_behaves_like 'handles image resize requests' + it_behaves_like 'handles image resize requests', :avatar + it_behaves_like 'handles image resize requests', :pwa_icon end context 'when CDN-enabled remote file is used' do diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 8806d1c2219..e3ec28f9c65 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -510,6 +510,33 @@ RSpec.describe 'Group', feature_category: :subgroups do end end end + + context 'when in a private group' do + before do + group.update!( + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS + ) + end + + context 'when visibility levels have been restricted to private only by an administrator' do + before do + stub_application_setting( + restricted_visibility_levels: [ + Gitlab::VisibilityLevel::PRIVATE + ] + ) + end + + it 'does not display the "New project" button' do + visit group_path(group) + + page.within '[data-testid="group-buttons"]' do + expect(page).not_to have_link('New project') + end + end + end + end end def remove_with_confirm(button_text, confirm_with) diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb index 9d3046a9a72..4beddb8c8bc 100644 --- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb @@ -92,7 +92,8 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_ page.execute_script("window.scrollTo(0,0)") end - it 'excludes resolved threads during navigation' do + it 'excludes resolved threads during navigation', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/383687' do goto_next_thread goto_next_thread goto_next_thread diff --git a/spec/finders/ci/pipelines_finder_spec.rb b/spec/finders/ci/pipelines_finder_spec.rb index 9ce3becf013..8773fbccdfc 100644 --- a/spec/finders/ci/pipelines_finder_spec.rb +++ b/spec/finders/ci/pipelines_finder_spec.rb @@ -246,9 +246,9 @@ RSpec.describe Ci::PipelinesFinder do let_it_be(:pipeline) { create(:ci_pipeline, project: project, name: 'Build pipeline') } let_it_be(:pipeline_other) { create(:ci_pipeline, project: project, name: 'Some other pipeline') } - let(:params) { { name: 'build Pipeline' } } + let(:params) { { name: 'Build pipeline' } } - it 'performs case insensitive compare' do + it 'performs exact compare' do is_expected.to contain_exactly(pipeline) end diff --git a/spec/fixtures/emails/html_only.eml b/spec/fixtures/emails/html_only.eml new file mode 100644 index 00000000000..22a1a431771 --- /dev/null +++ b/spec/fixtures/emails/html_only.eml @@ -0,0 +1,45 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +MIME-Version: 1.0 +In-Reply-To: <topic/22638/86406@meta.discourse.org> +References: <topic/22638@meta.discourse.org> + <topic/22638/86406@meta.discourse.org> +Date: Fri, 28 Nov 2014 12:36:49 -0800 +Subject: Re: [Discourse Meta] [Lounge] Testing default email replies +From: Walter White <walter.white@googlemail.com> +To: Discourse Meta <reply@discourse.org> +Content-Type: multipart/related; boundary=001a11c2e04e6544f30508f138ba + +--001a11c2e04e6544f30508f138ba +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr"><div>### This is a reply from standard GMail in Google Chr= +ome.</div><div><br></div><div>The quick brown fox jumps over the lazy dog. = +The quick brown fox jumps over the lazy dog. The quick brown fox jumps over= + the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown= + fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. = +The quick brown fox jumps over the lazy dog. The quick brown fox jumps over= + the lazy dog.=C2=A0</div><div><br></div><div>Here's some **bold** text= +, <strong>strong</strong> text and <em>italic</em> in Markdown.</div><div><= +br></div><div>Here's a link <a href=3D"http://example.com">http://examp= +le.com</a></div></div><div class=3D"gmail_extra"><br>Here's an img <i= +mg class="header__logoSize_110px" src="http://img.png" hspace="0" vspac= +e="0" border="0" style="display:block; width:138px; max-width:138px;" width= +="138" alt="Miro"><details><summary>One</summary> Some details</details> +<details><summary>Two</summary> Some details</details></div> + +<table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" bor= +der=3D"0"> + <tbody> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> + <p style=3D"margin-top:0;border:0">Test reply.</p> + <p style=3D"margin-top:0;border:0">First paragraph.</p> + <p style=3D"margin-top:0;border:0">Second paragraph.</p> + </td> + </tr> + </tbody> +</table> + +--001a11c2e04e6544f30508f138ba-- diff --git a/spec/fixtures/lib/gitlab/email/basic.html b/spec/fixtures/lib/gitlab/email/basic.html new file mode 100644 index 00000000000..807b23c46e3 --- /dev/null +++ b/spec/fixtures/lib/gitlab/email/basic.html @@ -0,0 +1,72 @@ +<html> + <title>Ignored Title</title> + <body> + <h1>Hello, World!</h1> + + This is some e-mail content. + Even though it has whitespace and newlines, the e-mail converter + will handle it correctly. + + <p><em>Even</em> mismatched tags.</p> + + <div>A div</div> + <div>Another div</div> + <div>A div<div><strong>within</strong> a div</div></div> + + <p>Another line<br />Yet another line</p> + + <a href="http://foo.com">A link</a> + + <p><details><summary>One</summary>Some details</details></p> + + <p><details><summary>Two</summary>Some details</details></p> + + <img class="header__logoSize_110px" src="http://img.png" hspace="0" vspace="0" border="0" + style="display:block; width:138px; max-width:138px;" width="138" alt="Miro"> + + <table> + <thead> + <tr> + <th>Col A</th> + <th>Col B</th> + </tr> + </thead> + <tbody> + <tr> + <td> + Data A1 + </td> + <td> + Data B1 + </td> + </tr> + <tr> + <td> + Data A2 + </td> + <td> + Data B2 + </td> + </tr> + <tr> + <td> + Data A3 + </td> + <td> + Data B4 + </td> + </tr> + </tbody> + <tfoot> + <tr> + <td> + Total A + </td> + <td> + Total B + </td> + </tr> + </tfoot> + </table> + </body> +</html> diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js index f61af6203f0..738cbd88c4c 100644 --- a/spec/frontend/admin/topics/components/topic_select_spec.js +++ b/spec/frontend/admin/topics/components/topic_select_spec.js @@ -1,39 +1,66 @@ -import { GlAvatarLabeled, GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAvatarLabeled, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import TopicSelect from '~/admin/topics/components/topic_select.vue'; +import searchProjectTopics from '~/graphql_shared/queries/project_topics_search.query.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; const mockTopics = [ - { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' }, - { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' }, + { + id: 'gid://gitlab/Projects::Topic/6', + name: 'topic1', + title: 'Topic 1', + avatarUrl: 'avatar.com/topic1.png', + __typename: 'Topic', + }, + { + id: 'gid://gitlab/Projects::Topic/5', + name: 'gitlab', + title: 'GitLab', + avatarUrl: 'avatar.com/GitLab.png', + __typename: 'Topic', + }, ]; +const mockTopicsQueryResponse = { + data: { + topics: { + nodes: mockTopics, + __typename: 'TopicConnection', + }, + }, +}; + describe('TopicSelect', () => { let wrapper; + const mockSearchTopicsSuccess = jest.fn().mockResolvedValue(mockTopicsQueryResponse); + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + function createMockApolloProvider({ mockSearchTopicsQuery = mockSearchTopicsSuccess } = {}) { + Vue.use(VueApollo); + + return createMockApollo([[searchProjectTopics, mockSearchTopicsQuery]]); + } - function createComponent(props = {}) { - wrapper = shallowMount(TopicSelect, { + function createComponent({ props = {}, mockApollo } = {}) { + wrapper = mount(TopicSelect, { + apolloProvider: mockApollo || createMockApolloProvider(), propsData: props, data() { return { topics: mockTopics, - search: '', }; }, - mocks: { - $apollo: { - queries: { - topics: { loading: false }, - }, - }, - }, }); } afterEach(() => { wrapper.destroy(); + jest.clearAllMocks(); }); it('mounts', () => { @@ -57,17 +84,27 @@ describe('TopicSelect', () => { it('renders default text if no selected topic', () => { createComponent(); - expect(findDropdown().props('text')).toBe('Select a topic'); + expect(findListbox().props('toggleText')).toBe('Select a topic'); }); it('renders selected topic', () => { - createComponent({ selectedTopic: mockTopics[0] }); + const mockTopic = mockTopics[0]; - expect(findDropdown().props('text')).toBe('topic1'); + createComponent({ + props: { + selectedTopic: mockTopic, + }, + }); + + expect(findListbox().props('toggleText')).toBe(mockTopic.name); }); it('renders label', () => { - createComponent({ labelText: 'my label' }); + createComponent({ + props: { + labelText: 'my label', + }, + }); expect(wrapper.find('label').text()).toBe('my label'); }); @@ -75,17 +112,52 @@ describe('TopicSelect', () => { it('renders dropdown items', () => { createComponent(); - const dropdownItems = findAllDropdownItems(); + const listboxItems = findAllListboxItems(); + + expect(listboxItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1'); + expect(listboxItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab'); + }); + + it('dropdown `toggledAriaLabelledBy` prop is not set if `labelText` prop is null', () => { + createComponent(); - expect(dropdownItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1'); - expect(dropdownItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab'); + expect(findListbox().props('toggle-aria-labelled-by')).toBe(undefined); }); - it('emits `click` event when topic selected', () => { + it('emits `click` event when topic selected', async () => { createComponent(); - findAllDropdownItems().at(0).vm.$emit('click'); + await findAllListboxItems().at(0).trigger('click'); expect(wrapper.emitted('click')).toEqual([[mockTopics[0]]]); }); + + describe('when searching a topic', () => { + const searchTopic = (searchTerm) => findListbox().vm.$emit('search', searchTerm); + const mockSearchTerm = 'gitl'; + + it('toggles loading state', async () => { + createComponent(); + jest.runOnlyPendingTimers(); + + await searchTopic(mockSearchTerm); + + expect(findListbox().props('searching')).toBe(true); + + await waitForPromises(); + + expect(findListbox().props('searching')).toBe(false); + }); + + it('fetches topics matching search string', async () => { + createComponent(); + + await searchTopic(mockSearchTerm); + jest.runOnlyPendingTimers(); + + expect(mockSearchTopicsSuccess).toHaveBeenCalledWith({ + search: mockSearchTerm, + }); + }); + }); }); diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js index 67d0fbdd9d1..ffcfd1d9f78 100644 --- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js +++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js @@ -110,17 +110,22 @@ describe('WikiForm', () => { it('displays markdown editor', () => { createWrapper({ persisted: true }); - expect(findMarkdownEditor().props()).toEqual( + const markdownEditor = findMarkdownEditor(); + + expect(markdownEditor.props()).toEqual( expect.objectContaining({ value: pageInfoPersisted.content, renderMarkdownPath: pageInfoPersisted.markdownPreviewPath, markdownDocsPath: pageInfoPersisted.markdownHelpPath, uploadsPath: pageInfoPersisted.uploadsPath, autofocus: pageInfoPersisted.persisted, - formFieldId: 'wiki_content', - formFieldName: 'wiki[content]', }), ); + + expect(markdownEditor.props('formFieldProps')).toMatchObject({ + id: 'wiki_content', + name: 'wiki[content]', + }); }); it.each` diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js index 518539d97ba..2523b901506 100644 --- a/spec/frontend/pipelines/pipelines_spec.js +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -449,6 +449,26 @@ describe('Pipelines', () => { `${window.location.pathname}?page=2&scope=all`, ); }); + + it('should reset page to 1 when filtering pipelines', () => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?page=2&scope=all`, + ); + + findFilteredSearch().vm.$emit('submit', [ + { type: 'status', value: { data: 'success', operator: '=' } }, + ]); + + expect(window.history.pushState).toHaveBeenCalledTimes(2); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + `${window.location.pathname}?page=1&scope=all&status=success`, + ); + }); }); }); diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js index e3df2cde1c1..12eda284aea 100644 --- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js +++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js @@ -36,10 +36,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => { quickActionsDocsPath, enableAutocomplete, enablePreview, - formFieldId, - formFieldName, - formFieldPlaceholder, - formFieldAriaLabel, + formFieldProps: { + id: formFieldId, + name: formFieldName, + placeholder: formFieldPlaceholder, + 'aria-label': formFieldAriaLabel, + }, ...propsData, }, stubs: { @@ -95,6 +97,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => { expect(findTextarea().element.value).toBe(value); }); + it('fails to render if textarea id and name is not passed', () => { + expect(() => { + buildWrapper({ propsData: { formFieldProps: {} } }); + }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"'); + }); + it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => { buildWrapper(); diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index 083bb5bc4a4..0b6ab5c3290 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -85,7 +85,7 @@ describe('WorkItemLabels component', () => { it('focuses token selector on token selector input event', async () => { createComponent(); findTokenSelector().vm.$emit('input', [mockLabels[0]]); - await nextTick(); + await waitForPromises(); expect(findEmptyState().exists()).toBe(false); expect(findTokenSelector().element.contains(document.activeElement)).toBe(true); @@ -189,6 +189,23 @@ describe('WorkItemLabels component', () => { ); }); + it('adds new labels to the end', async () => { + const response = workItemResponseFactory({ labels: [mockLabels[1]] }); + const workItemQueryHandler = jest.fn().mockResolvedValue(response); + createComponent({ + workItemQueryHandler, + updateWorkItemMutationHandler: successUpdateWorkItemMutationHandler, + }); + await waitForPromises(); + + findTokenSelector().vm.$emit('input', [mockLabels[0]]); + await waitForPromises(); + + const labels = findTokenSelector().props('selectedTokens'); + expect(labels[0]).toMatchObject(mockLabels[1]); + expect(labels[1]).toMatchObject(mockLabels[0]); + }); + describe('when clicking outside the token selector', () => { it('calls a mutation with correct variables', () => { createComponent(); @@ -205,9 +222,7 @@ describe('WorkItemLabels component', () => { }); it('emits an error and resets labels if mutation was rejected', async () => { - const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); - - createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler }); + createComponent({ updateWorkItemMutationHandler: errorHandler }); await waitForPromises(); @@ -224,6 +239,23 @@ describe('WorkItemLabels component', () => { expect(updatedLabels).toEqual(initialLabels); }); + it('does not make server request if no labels added or removed', async () => { + const updateWorkItemMutationHandler = jest + .fn() + .mockResolvedValue(updateWorkItemMutationResponse); + + createComponent({ updateWorkItemMutationHandler }); + + await waitForPromises(); + + findTokenSelector().vm.$emit('input', []); + findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null })); + + await waitForPromises(); + + expect(updateWorkItemMutationHandler).not.toHaveBeenCalled(); + }); + it('has a subscription', async () => { createComponent(); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 5b331c016a9..d6b2b5a1981 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -215,6 +215,14 @@ export const updateWorkItemMutationResponse = { nodes: [mockAssignees[0]], }, }, + { + __typename: 'WorkItemWidgetLabels', + type: 'LABELS', + allowsScopedLabels: false, + labels: { + nodes: mockLabels, + }, + }, ], }, }, @@ -279,7 +287,6 @@ export const workItemResponseFactory = ({ allowsMultipleAssignees = true, assigneesWidgetPresent = true, datesWidgetPresent = true, - labelsWidgetPresent = true, weightWidgetPresent = true, progressWidgetPresent = true, milestoneWidgetPresent = true, @@ -288,6 +295,8 @@ export const workItemResponseFactory = ({ notesWidgetPresent = true, confidential = false, canInviteMembers = false, + labelsWidgetPresent = true, + labels = mockLabels, allowsScopedLabels = false, lastEditedAt = null, lastEditedBy = null, @@ -350,7 +359,7 @@ export const workItemResponseFactory = ({ type: 'LABELS', allowsScopedLabels, labels: { - nodes: mockLabels, + nodes: labels, }, } : { type: 'MOCK TYPE' }, diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb index 3c698fb2d41..2b0192d24b3 100644 --- a/spec/helpers/appearances_helper_spec.rb +++ b/spec/helpers/appearances_helper_spec.rb @@ -10,6 +10,51 @@ RSpec.describe AppearancesHelper do allow(helper).to receive(:current_user).and_return(user) end + describe 'pwa icon scaled' do + before do + stub_config_setting(relative_url_root: '/relative_root') + end + + shared_examples 'gets icon path' do |width| + let!(:width) { width } + + it 'returns path of icon' do + expect(helper.appearance_pwa_icon_path_scaled(width)).to match(result) + end + end + + context 'with custom icon' do + let!(:appearance) { create(:appearance, :with_pwa_icon) } + let!(:result) { "/relative_root/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=#{width}" } + + it_behaves_like 'gets icon path', 192 + it_behaves_like 'gets icon path', 512 + end + + context 'with default icon' do + let!(:result) { "/relative_root/-/pwa-icons/logo-#{width}.png" } + + it_behaves_like 'gets icon path', 192 + it_behaves_like 'gets icon path', 512 + end + + it 'returns path of maskable logo' do + expect(helper.appearance_maskable_logo).to match('/relative_root/-/pwa-icons/maskable-logo.png') + end + + context 'with wrong input' do + let!(:result) { nil } + + it_behaves_like 'gets icon path', 19200 + end + + context 'when path is append to root' do + it 'appends root and path' do + expect(helper.append_root_path('/works_just_fine')).to match('/relative_root/works_just_fine') + end + end + end + describe '#appearance_pwa_name' do it 'returns the default value' do create(:appearance) diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 3a885d70eb4..1fb442a74fb 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -73,9 +73,8 @@ RSpec.describe Gitlab::BareRepositoryImport::Importer do end it 'does not schedule an import' do - expect_next_instance_of(Project) do |instance| - expect(instance).not_to receive(:import_schedule) - end + project = Project.find_by_full_path(project_path) + expect(project).not_to receive(:import_schedule) importer.create_project_if_needed end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index 236e04a041b..95b1661ac99 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -27,8 +27,8 @@ RSpec.describe Gitlab::BitbucketImport::ProjectCreator do end it 'creates project' do - expect_next_instance_of(Project) do |project| - expect(project).to receive(:add_import_job) + allow_next_instances_of(Project, 2) do |project| + allow(project).to receive(:add_import_job) end project_creator = described_class.new(repo, 'vim', namespace, user, access_params) diff --git a/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb new file mode 100644 index 00000000000..fe585d47d59 --- /dev/null +++ b/spec/lib/gitlab/email/html_to_markdown_parser_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Email::HtmlToMarkdownParser, feature_category: :service_desk do + subject { described_class.convert(html) } + + describe '.convert' do + let(:html) { fixture_file("lib/gitlab/email/basic.html") } + + it 'parses html correctly' do + expect(subject) + .to eq( + <<-BODY.strip_heredoc.chomp + Hello, World! + This is some e-mail content. Even though it has whitespace and newlines, the e-mail converter will handle it correctly. + *Even* mismatched tags. + A div + Another div + A div + **within** a div + + Another line + Yet another line + [A link](http://foo.com) + <details> + <summary> + One</summary> + Some details</details> + + <details> + <summary> + Two</summary> + Some details</details> + + ![Miro](http://img.png) + Col A Col B + Data A1 Data B1 + Data A2 Data B2 + Data A3 Data B4 + Total A Total B + BODY + ) + end + end +end diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb index 10ffb420508..e4c68dbba92 100644 --- a/spec/lib/gitlab/email/reply_parser_spec.rb +++ b/spec/lib/gitlab/email/reply_parser_spec.rb @@ -188,6 +188,70 @@ RSpec.describe Gitlab::Email::ReplyParser do ) end + context 'properly renders email reply from gmail web client' do + context 'when feature flag is enabled' do + it do + expect(test_parse_body(fixture_file("emails/html_only.eml"))) + .to eq( + <<-BODY.strip_heredoc.chomp + ### This is a reply from standard GMail in Google Chrome. + + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + + Here's some **bold** text, **strong** text and *italic* in Markdown. + + Here's a link http://example.com + + Here's an img ![Miro](http://img.png)<details> + <summary> + One</summary> + Some details</details> + + <details> + <summary> + Two</summary> + Some details</details> + + Test reply. + + First paragraph. + + Second paragraph. + BODY + ) + end + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(service_desk_html_to_text_email_handler: false) + end + + it do + expect(test_parse_body(fixture_file("emails/html_only.eml"))) + .to eq( + <<-BODY.strip_heredoc.chomp + ### This is a reply from standard GMail in Google Chrome. + + The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. + + Here's some **bold** text, strong text and italic in Markdown. + + Here's a link http://example.com + + Here's an img [Miro]One Some details Two Some details + + Test reply. + + First paragraph. + + Second paragraph. + BODY + ) + end + end + end + it "properly renders email reply from iOS default mail client" do expect(test_parse_body(fixture_file("emails/ios_default.eml"))) .to eq( diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb index 53bf1db3438..59a98987f7d 100644 --- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb +++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb @@ -24,8 +24,8 @@ RSpec.describe Gitlab::GitlabImport::ProjectCreator do end it 'creates project' do - expect_next_instance_of(Project) do |project| - expect(project).to receive(:add_import_job) + allow_next_instance_of(Project) do |project| + allow(project).to receive(:add_import_job) end project_creator = described_class.new(repo, namespace, user, access_params) diff --git a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb index f853bccc115..103d3512e8b 100644 --- a/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb +++ b/spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb @@ -295,9 +295,9 @@ RSpec.describe Gitlab::ImportExport::Json::StreamingSerializer, feature_category end end - it_behaves_like 'record with exportable associations', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/390356' do + it_behaves_like 'record with exportable associations' do let(:expected_issue) do - issue_hash[many_relation].delete_at(1) + issue_hash[many_relation].delete_if { |record| record['id'] == link2.id } issue_hash.to_json(options) end end diff --git a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb index 936c63fd6cd..d133f54ade5 100644 --- a/spec/lib/gitlab/import_export/project/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/project/relation_factory_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_memory_store_caching do +RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_memory_store_caching, feature_category: :importers do let(:group) { create(:group).tap { |g| g.add_maintainer(importer_user) } } let(:project) { create(:project, :repository, group: group) } let(:members_mapper) { double('members_mapper').as_null_object } @@ -418,21 +418,73 @@ RSpec.describe Gitlab::ImportExport::Project::RelationFactory, :use_clean_rails_ end end - context 'merge request access level object' do - let(:relation_sym) { :'ProtectedBranch::MergeAccessLevel' } - let(:relation_hash) { { 'access_level' => 30, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } } + describe 'protected branch access levels' do + shared_examples 'access levels' do + let(:relation_hash) { { 'access_level' => access_level, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } } - it 'sets access level to maintainer' do - expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + context 'when access level is no one' do + let(:access_level) { Gitlab::Access::NO_ACCESS } + + it 'keeps no one access level' do + expect(created_object.access_level).to equal(access_level) + end + end + + context 'when access level is below maintainer' do + let(:access_level) { Gitlab::Access::DEVELOPER } + + it 'sets access level to maintainer' do + expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + end + end + + context 'when access level is above maintainer' do + let(:access_level) { Gitlab::Access::OWNER } + + it 'sets access level to maintainer' do + expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + end + end + + describe 'root ancestor membership' do + let(:access_level) { Gitlab::Access::DEVELOPER } + + context 'when importer user is root group owner' do + let(:importer_user) { create(:user) } + + it 'keeps access level as is' do + group.add_owner(importer_user) + + expect(created_object.access_level).to equal(access_level) + end + end + + context 'when user membership in root group is missing' do + it 'sets access level to maintainer' do + group.members.delete_all + + expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + end + end + + context 'when root ancestor is not a group' do + it 'sets access level to maintainer' do + expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + end + end + end + end + + describe 'merge access level' do + let(:relation_sym) { :'ProtectedBranch::MergeAccessLevel' } + + include_examples 'access levels' end - end - context 'push access level object' do - let(:relation_sym) { :'ProtectedBranch::PushAccessLevel' } - let(:relation_hash) { { 'access_level' => 30, 'created_at' => '2022-03-29T09:53:13.457Z', 'updated_at' => '2022-03-29T09:54:13.457Z' } } + describe 'push access level' do + let(:relation_sym) { :'ProtectedBranch::PushAccessLevel' } - it 'sets access level to maintainer' do - expect(created_object.access_level).to equal(Gitlab::Access::MAINTAINER) + include_examples 'access levels' end end end diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index 17ecd183ac9..5df44bfb83c 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::LegacyGithubImport::ProjectCreator do before do namespace.add_owner(user) - expect_next_instance_of(Project) do |project| + allow_next_instances_of(Project, 2) do |project| allow(project).to receive(:add_import_job) end end diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index 0a1de1d5dc4..b5f47c950b9 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -26,6 +26,7 @@ RSpec.describe Appearance do it { expect(appearance.message_background_color).to eq('#E75E40') } it { expect(appearance.message_font_color).to eq('#FFFFFF') } it { expect(appearance.email_header_and_footer_enabled).to eq(false) } + it { expect(Appearance::ALLOWED_PWA_ICON_SCALER_WIDTHS).to match_array([192, 512]) } end describe '#single_appearance_row' do @@ -84,6 +85,19 @@ RSpec.describe Appearance do it_behaves_like 'logo paths', logo_type end + shared_examples 'icon paths sized' do |width| + let_it_be(:appearance) { create(:appearance, :with_pwa_icon) } + let_it_be(:filename) { 'dk.png' } + let_it_be(:expected_path) { "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/#{filename}?width=#{width}" } + + it 'returns icon path with size parameter' do + expect(appearance.pwa_icon_path_scaled(width)).to eq(expected_path) + end + end + + it_behaves_like 'icon paths sized', 192 + it_behaves_like 'icon paths sized', 512 + describe 'validations' do let(:triplet) { '#000' } let(:hex) { '#AABBCC' } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 5888f9d109c..4a59f8d8efc 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -226,9 +226,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline') } context 'when name exists' do - let(:name) { 'build Pipeline' } + let(:name) { 'Build pipeline' } - it 'performs case insensitive compare' do + it 'performs exact compare' do is_expected.to contain_exactly(pipeline1) end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 451db9eaf9c..668b3aa8236 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -667,6 +667,42 @@ RSpec.describe GroupPolicy, feature_category: :authentication_and_authorization it { is_expected.to be_allowed(:create_projects) } end + + context 'when there are no available visibility levels because they have been restricted by an administrator' do + before do + stub_application_setting( + restricted_visibility_levels: [ + Gitlab::VisibilityLevel::PUBLIC, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PRIVATE + ] + ) + end + + context 'reporter' do + let(:current_user) { reporter } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_disallowed(:create_projects) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_disallowed(:create_projects) } + end + end end end diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb index cff8c34e4a1..b8331e072cf 100644 --- a/spec/requests/api/draft_notes_spec.rb +++ b/spec/requests/api/draft_notes_spec.rb @@ -9,8 +9,8 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } - let_it_be(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) } - let_it_be(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) } + let!(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) } + let!(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) } before do project.add_developer(user) @@ -74,4 +74,47 @@ RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do end end end + + describe "delete a draft note" do + context "when deleting an existing draft note by the user" do + let!(:deleted_draft_note_id) { draft_note_by_current_user.id } + + before do + delete api( + "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_current_user.id}", + user + ) + end + + it "returns 204 No Content status" do + expect(response).to have_gitlab_http_status(:no_content) + end + + it "deletes the specified draft note" do + expect(DraftNote.exists?(deleted_draft_note_id)).to eq(false) + end + end + + context "when deleting a non-existent draft note" do + it "returns a 404 Not Found" do + delete api( + "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{non_existing_record_id}", + user + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when deleting a draft note by a different user" do + it "returns a 404 Not Found" do + delete api( + "/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes/#{draft_note_by_random_user.id}", + user + ) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end end diff --git a/spec/requests/pwa_controller_spec.rb b/spec/requests/pwa_controller_spec.rb index 393f8803375..08eeefd1dc4 100644 --- a/spec/requests/pwa_controller_spec.rb +++ b/spec/requests/pwa_controller_spec.rb @@ -4,28 +4,74 @@ require 'spec_helper' RSpec.describe PwaController, feature_category: :navigation do describe 'GET #manifest' do - it 'responds with json' do - get manifest_path(format: :json) + shared_examples 'text values' do |params, result| + let_it_be(:appearance) { create(:appearance, **params) } - expect(Gitlab::Json.parse(response.body)).to include({ 'name' => 'GitLab' }) - expect(Gitlab::Json.parse(response.body)).to include({ 'short_name' => 'GitLab' }) - expect(response.body).to include('The complete DevOps platform.') - expect(response).to have_gitlab_http_status(:success) + it 'uses custom values', :aggregate_failures do + get manifest_path(format: :json) + + expect(Gitlab::Json.parse(response.body)).to include(result) + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'with default appearance' do + it_behaves_like 'text values', {}, { + 'name' => 'GitLab', + 'short_name' => 'GitLab', + 'description' => 'The complete DevOps platform. ' \ + 'One application with endless possibilities. ' \ + 'Organizations rely on GitLab’s source code management, ' \ + 'CI/CD, security, and more to deliver software rapidly.' + } end context 'with customized appearance' do - let_it_be(:appearance) do - create(:appearance, pwa_name: 'PWA name', pwa_short_name: 'Short name', pwa_description: 'This is a test') + context 'with custom text values' do + it_behaves_like 'text values', { pwa_name: 'PWA name' }, { 'name' => 'PWA name' } + it_behaves_like 'text values', { pwa_short_name: 'Short name' }, { 'short_name' => 'Short name' } + it_behaves_like 'text values', { pwa_description: 'This is a test' }, { 'description' => 'This is a test' } end - it 'uses custom values', :aggregate_failures do - get manifest_path(format: :json) + shared_examples 'icon paths' do + it 'returns expected icon paths', :aggregate_failures do + get manifest_path(format: :json) + + expect(Gitlab::Json.parse(response.body)["icons"]).to match_array(result) + expect(response).to have_gitlab_http_status(:success) + end + end + + context 'with custom icon' do + let_it_be(:appearance) { create(:appearance, :with_pwa_icon) } + let_it_be(:result) do + [{ "src" => "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=192", "sizes" => "192x192", + "type" => "image/png" }, + { "src" => "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/dk.png?width=512", "sizes" => "512x512", + "type" => "image/png" }] + end + + it_behaves_like 'icon paths' + end - expect(Gitlab::Json.parse(response.body)).to include({ - 'description' => 'This is a test', - 'name' => 'PWA name', - 'short_name' => 'Short name' - }) + context 'with no custom icon' do + let_it_be(:appearance) { create(:appearance) } + let_it_be(:result) do + [{ "src" => "/-/pwa-icons/logo-192.png", "sizes" => "192x192", "type" => "image/png" }, + { "src" => "/-/pwa-icons/logo-512.png", "sizes" => "512x512", "type" => "image/png" }, + { "src" => "/-/pwa-icons/maskable-logo.png", "sizes" => "512x512", "type" => "image/png", + "purpose" => "maskable" }] + end + + it_behaves_like 'icon paths' + end + end + + describe 'GET #offline' do + it 'responds with static HTML page' do + get offline_path + + expect(response.body).to include('You are currently offline') expect(response).to have_gitlab_http_status(:success) end end @@ -47,13 +93,4 @@ RSpec.describe PwaController, feature_category: :navigation do end end end - - describe 'GET #offline' do - it 'responds with static HTML page' do - get offline_path - - expect(response.body).to include('You are currently offline') - expect(response).to have_gitlab_http_status(:success) - end - end end |