diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-24 18:15:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-24 18:15:02 +0300 |
commit | c4b4a75c35cb2015c01ef0b60f8ad8baaaf889df (patch) | |
tree | 16eabfd63477e1904d7eb5d9f92f3e5a4e4d3e0f /spec | |
parent | e40c68997d44209aed2baf3a8ec6be9ae99fb0b5 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
23 files changed, 545 insertions, 122 deletions
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index fbdba373843..90b64cde935 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -313,7 +313,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do context 'job is cancelable' do it 'shows cancel button' do - click_link 'Cancel' + find('[data-testid="cancel-button"]').click expect(page.current_path).to eq(job_url) end @@ -1031,7 +1031,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do end it 'loads the page and shows all needed controls' do - expect(page).to have_content 'Retry' + expect(page).to have_selector('[data-testid="retry-button"') end end end @@ -1049,7 +1049,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do it 'shows the right status and buttons' do page.within('aside.right-sidebar') do - expect(page).to have_content 'Cancel' + expect(page).to have_selector('[data-testid="cancel-button"') end end end diff --git a/spec/fixtures/emails/missing_delivered_to_header.eml b/spec/fixtures/emails/missing_delivered_to_header.eml new file mode 100644 index 00000000000..511f60ab719 --- /dev/null +++ b/spec/fixtures/emails/missing_delivered_to_header.eml @@ -0,0 +1,35 @@ +Return-Path: <jake@example.com> +Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Received: from blabla.google.com (blabla.google.com. [1.1.1.1]) + by bla.google.com with SMTPS id something.1.1.1.1.1.1.1 + for <incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com> + (Google Transport Security); + Mon, 21 Feb 2022 14:41:58 -0800 (PST) +Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@example.com>; Thu, 13 Jun 2013 17:03:50 -0400 +From: "jake@example.com" <jake@example.com> +To: "support@example.com" <support@example.com> +Subject: Insert hilarious subject line here +Date: Tue, 26 Nov 2019 14:22:41 +0000 +Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT> +Accept-Language: de-DE, en-US +Content-Language: de-DE +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-ms-exchange-transport-fromentityheader: Hosted +x-originating-ip: [62.96.54.178] +Content-Type: multipart/alternative; + boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_" +MIME-Version: 1.0 + +--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + + + +--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_ +Content-Type: text/html; charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Look, a message with no Delivered-To header! Let's fallback to Received: in case it's there. diff --git a/spec/fixtures/emails/valid_note_on_issuable.eml b/spec/fixtures/emails/valid_note_on_issuable.eml index 29308c9d969..38b733b6a32 100644 --- a/spec/fixtures/emails/valid_note_on_issuable.eml +++ b/spec/fixtures/emails/valid_note_on_issuable.eml @@ -1,6 +1,6 @@ Return-Path: <jake@adventuretime.ooo> Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 -Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq-gitlabhq-project_id-auth_token-issue-issue_iid@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq-gitlabhq-project_id-auth_token-issue-issue_iid@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 Date: Thu, 13 Jun 2013 17:03:48 -0400 diff --git a/spec/fixtures/error_tracking/php_empty_transaction.json b/spec/fixtures/error_tracking/php_empty_transaction.json new file mode 100644 index 00000000000..fc51894145d --- /dev/null +++ b/spec/fixtures/error_tracking/php_empty_transaction.json @@ -0,0 +1,45 @@ +{ + "event_id": "dquJXuPF9sP1fMy5RpKo979xUALjNDQB", + "timestamp": 1645191605.123456, + "platform": "php", + "sdk": { + "name": "sentry.php", + "version": "3.3.7" + }, + "logger": "php", + "transaction": "", + "server_name": "oAjA5zTgIjqP", + "release": "C0FFEE", + "environment": "Development/Berlin", + "exception": { + "values": [ + { + "type": "TestException", + "value": "Sentry test exception", + "stacktrace": { + "frames": [ + { + "filename": "/src/Path/To/Class.php", + "lineno": 3, + "in_app": true, + "abs_path": "/var/www/html/src/Path/To/Class.php", + "function": "Path\\To\\Class::method", + "raw_function": "Path\\To\\Class::method", + "pre_context": [ + "// Pre-context" + ], + "context_line": "throw new TestException('Sentry test exception');", + "post_context": [ + "// Post-context" + ] + } + ] + }, + "mechanism": { + "type": "generic", + "handled": true + } + } + ] + } +} diff --git a/spec/frontend/branches/ajax_loading_spinner_spec.js b/spec/frontend/branches/ajax_loading_spinner_spec.js deleted file mode 100644 index 31cc7b99e42..00000000000 --- a/spec/frontend/branches/ajax_loading_spinner_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner'; - -describe('Ajax Loading Spinner', () => { - let ajaxLoadingSpinnerElement; - let fauxEvent; - beforeEach(() => { - document.body.innerHTML = ` - <div> - <a class="js-ajax-loading-spinner" - data-remote - href="http://goesnowhere.nothing/whereami"> - Remove me - </a></div>`; - AjaxLoadingSpinner.init(); - ajaxLoadingSpinnerElement = document.querySelector('.js-ajax-loading-spinner'); - fauxEvent = { target: ajaxLoadingSpinnerElement }; - }); - - afterEach(() => { - document.body.innerHTML = ''; - }); - - it('`ajaxBeforeSend` event handler sets current icon to spinner and disables link', () => { - expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).toBeNull(); - expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(false); - - AjaxLoadingSpinner.ajaxBeforeSend(fauxEvent); - - expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).not.toBeNull(); - expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(true); - }); -}); diff --git a/spec/frontend/content_editor/components/content_editor_alert_spec.js b/spec/frontend/content_editor/components/content_editor_alert_spec.js index 2ddcd8f024e..12484cb13c6 100644 --- a/spec/frontend/content_editor/components/content_editor_alert_spec.js +++ b/spec/frontend/content_editor/components/content_editor_alert_spec.js @@ -3,20 +3,25 @@ import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue'; import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; -import { createTestEditor, emitEditorEvent } from '../test_utils'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import { ALERT_EVENT } from '~/content_editor/constants'; +import { createTestEditor } from '../test_utils'; describe('content_editor/components/content_editor_alert', () => { let wrapper; let tiptapEditor; + let eventHub; const findErrorAlert = () => wrapper.findComponent(GlAlert); const createWrapper = async () => { tiptapEditor = createTestEditor(); + eventHub = eventHubFactory(); wrapper = shallowMountExtended(ContentEditorAlert, { provide: { tiptapEditor, + eventHub, }, stubs: { EditorStateObserver, @@ -37,7 +42,9 @@ describe('content_editor/components/content_editor_alert', () => { async ({ message, variant }) => { createWrapper(); - await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } }); + eventHub.$emit(ALERT_EVENT, { message, variant }); + + await nextTick(); expect(findErrorAlert().text()).toBe(message); expect(findErrorAlert().attributes().variant).toBe(variant); @@ -48,11 +55,9 @@ describe('content_editor/components/content_editor_alert', () => { const message = 'error message'; createWrapper(); - - await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } }); - + eventHub.$emit(ALERT_EVENT, { message }); + await nextTick(); findErrorAlert().vm.$emit('dismiss'); - await nextTick(); expect(findErrorAlert().exists()).toBe(false); diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js index 9a772c41e52..a713211b6f4 100644 --- a/spec/frontend/content_editor/components/content_editor_spec.js +++ b/spec/frontend/content_editor/components/content_editor_spec.js @@ -121,7 +121,7 @@ describe('ContentEditor', () => { beforeEach(async () => { createWrapper(); - contentEditor.emit(LOADING_CONTENT_EVENT); + contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT); await nextTick(); }); @@ -143,9 +143,9 @@ describe('ContentEditor', () => { beforeEach(async () => { createWrapper(); - contentEditor.emit(LOADING_CONTENT_EVENT); + contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT); await nextTick(); - contentEditor.emit(LOADING_SUCCESS_EVENT); + contentEditor.eventHub.$emit(LOADING_SUCCESS_EVENT); await nextTick(); }); @@ -164,9 +164,9 @@ describe('ContentEditor', () => { beforeEach(async () => { createWrapper(); - contentEditor.emit(LOADING_CONTENT_EVENT); + contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT); await nextTick(); - contentEditor.emit(LOADING_ERROR_EVENT, error); + contentEditor.eventHub.$emit(LOADING_ERROR_EVENT, error); await nextTick(); }); diff --git a/spec/frontend/content_editor/components/editor_state_observer_spec.js b/spec/frontend/content_editor/components/editor_state_observer_spec.js index 5e4bb348e1f..51a594a606b 100644 --- a/spec/frontend/content_editor/components/editor_state_observer_spec.js +++ b/spec/frontend/content_editor/components/editor_state_observer_spec.js @@ -3,6 +3,13 @@ import { each } from 'lodash'; import EditorStateObserver, { tiptapToComponentMap, } from '~/content_editor/components/editor_state_observer.vue'; +import eventHubFactory from '~/helpers/event_hub_factory'; +import { + LOADING_CONTENT_EVENT, + LOADING_SUCCESS_EVENT, + LOADING_ERROR_EVENT, + ALERT_EVENT, +} from '~/content_editor/constants'; import { createTestEditor } from '../test_utils'; describe('content_editor/components/editor_state_observer', () => { @@ -11,19 +18,29 @@ describe('content_editor/components/editor_state_observer', () => { let onDocUpdateListener; let onSelectionUpdateListener; let onTransactionListener; + let onLoadingContentListener; + let onLoadingSuccessListener; + let onLoadingErrorListener; + let onAlertListener; + let eventHub; const buildEditor = () => { tiptapEditor = createTestEditor(); + eventHub = eventHubFactory(); jest.spyOn(tiptapEditor, 'on'); }; const buildWrapper = () => { wrapper = shallowMount(EditorStateObserver, { - provide: { tiptapEditor }, + provide: { tiptapEditor, eventHub }, listeners: { docUpdate: onDocUpdateListener, selectionUpdate: onSelectionUpdateListener, transaction: onTransactionListener, + [ALERT_EVENT]: onAlertListener, + [LOADING_CONTENT_EVENT]: onLoadingContentListener, + [LOADING_SUCCESS_EVENT]: onLoadingSuccessListener, + [LOADING_ERROR_EVENT]: onLoadingErrorListener, }, }); }; @@ -32,8 +49,11 @@ describe('content_editor/components/editor_state_observer', () => { onDocUpdateListener = jest.fn(); onSelectionUpdateListener = jest.fn(); onTransactionListener = jest.fn(); + onAlertListener = jest.fn(); + onLoadingSuccessListener = jest.fn(); + onLoadingContentListener = jest.fn(); + onLoadingErrorListener = jest.fn(); buildEditor(); - buildWrapper(); }); afterEach(() => { @@ -44,6 +64,8 @@ describe('content_editor/components/editor_state_observer', () => { it('emits update, selectionUpdate, and transaction events', () => { const content = '<p>My paragraph</p>'; + buildWrapper(); + tiptapEditor.commands.insertContent(content); expect(onDocUpdateListener).toHaveBeenCalledWith( @@ -58,10 +80,27 @@ describe('content_editor/components/editor_state_observer', () => { }); }); + it.each` + event | listener + ${ALERT_EVENT} | ${() => onAlertListener} + ${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener} + ${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener} + ${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener} + `('listens to $event event in the eventBus object', ({ event, listener }) => { + const args = {}; + + buildWrapper(); + + eventHub.$emit(event, args); + expect(listener()).toHaveBeenCalledWith(args); + }); + describe('when component is destroyed', () => { it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => { jest.spyOn(tiptapEditor, 'off'); + buildWrapper(); + wrapper.destroy(); each(tiptapToComponentMap, (_, tiptapEvent) => { @@ -71,5 +110,25 @@ describe('content_editor/components/editor_state_observer', () => { ); }); }); + + it.each` + event + ${ALERT_EVENT} + ${LOADING_CONTENT_EVENT} + ${LOADING_SUCCESS_EVENT} + ${LOADING_ERROR_EVENT} + `('removes $event event hook from eventHub', ({ event }) => { + jest.spyOn(eventHub, '$off'); + jest.spyOn(eventHub, '$on'); + + buildWrapper(); + + wrapper.destroy(); + + expect(eventHub.$off).toHaveBeenCalledWith( + event, + eventHub.$on.mock.calls.find(([eventName]) => eventName === event)[1], + ); + }); }); }); diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js index 60263c46bdd..ce50482302d 100644 --- a/spec/frontend/content_editor/components/toolbar_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_button_spec.js @@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue'; import ToolbarButton from '~/content_editor/components/toolbar_button.vue'; +import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; describe('content_editor/components/toolbar_button', () => { @@ -25,6 +26,7 @@ describe('content_editor/components/toolbar_button', () => { }, provide: { tiptapEditor, + eventHub: eventHubFactory(), }, propsData: { contentType: CONTENT_TYPE, diff --git a/spec/frontend/content_editor/components/toolbar_link_button_spec.js b/spec/frontend/content_editor/components/toolbar_link_button_spec.js index 0cf488260bd..fc26a9da471 100644 --- a/spec/frontend/content_editor/components/toolbar_link_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_link_button_spec.js @@ -1,6 +1,7 @@ import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue'; +import eventHubFactory from '~/helpers/event_hub_factory'; import Link from '~/content_editor/extensions/link'; import { hasSelection } from '~/content_editor/services/utils'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; @@ -15,6 +16,7 @@ describe('content_editor/components/toolbar_link_button', () => { wrapper = mountExtended(ToolbarLinkButton, { provide: { tiptapEditor: editor, + eventHub: eventHubFactory(), }, }); }; diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js index 65c1c8c8310..608be1bd693 100644 --- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js +++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js @@ -4,6 +4,7 @@ import EditorStateObserver from '~/content_editor/components/editor_state_observ import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue'; import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants'; import Heading from '~/content_editor/extensions/heading'; +import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils'; describe('content_editor/components/toolbar_text_style_dropdown', () => { @@ -27,6 +28,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => { }, provide: { tiptapEditor, + eventHub: eventHubFactory(), }, propsData: { ...propsData, diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index d2d2cd98a78..e095a3d0b6a 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -5,6 +5,7 @@ import Image from '~/content_editor/extensions/image'; import Link from '~/content_editor/extensions/link'; import Loading from '~/content_editor/extensions/loading'; import httpStatus from '~/lib/utils/http_status'; +import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor, createDocBuilder } from '../test_utils'; const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto"> @@ -25,6 +26,7 @@ describe('content_editor/extensions/attachment', () => { let link; let renderMarkdown; let mock; + let eventHub; const uploadsPath = '/uploads/'; const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); @@ -50,9 +52,15 @@ describe('content_editor/extensions/attachment', () => { beforeEach(() => { renderMarkdown = jest.fn(); + eventHub = eventHubFactory(); tiptapEditor = createTestEditor({ - extensions: [Loading, Link, Image, Attachment.configure({ renderMarkdown, uploadsPath })], + extensions: [ + Loading, + Link, + Image, + Attachment.configure({ renderMarkdown, uploadsPath, eventHub }), + ], }); ({ @@ -160,7 +168,7 @@ describe('content_editor/extensions/attachment', () => { it('emits an alert event that includes an error message', (done) => { tiptapEditor.commands.uploadAttachment({ file: imageFile }); - tiptapEditor.on('alert', ({ message }) => { + eventHub.$on('alert', ({ message }) => { expect(message).toBe('An error occurred while uploading the image. Please try again.'); done(); }); @@ -236,7 +244,7 @@ describe('content_editor/extensions/attachment', () => { it('emits an alert event that includes an error message', (done) => { tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); - tiptapEditor.on('alert', ({ message }) => { + eventHub.$on('alert', ({ message }) => { expect(message).toBe('An error occurred while uploading the file. Please try again.'); done(); }); diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js index e48687f1548..ac4f71a80cb 100644 --- a/spec/frontend/content_editor/services/content_editor_spec.js +++ b/spec/frontend/content_editor/services/content_editor_spec.js @@ -4,19 +4,21 @@ import { LOADING_ERROR_EVENT, } from '~/content_editor/constants'; import { ContentEditor } from '~/content_editor/services/content_editor'; - +import eventHubFactory from '~/helpers/event_hub_factory'; import { createTestEditor } from '../test_utils'; describe('content_editor/services/content_editor', () => { let contentEditor; let serializer; + let eventHub; beforeEach(() => { const tiptapEditor = createTestEditor(); jest.spyOn(tiptapEditor, 'destroy'); serializer = { deserialize: jest.fn() }; - contentEditor = new ContentEditor({ tiptapEditor, serializer }); + eventHub = eventHubFactory(); + contentEditor = new ContentEditor({ tiptapEditor, serializer, eventHub }); }); describe('.dispose', () => { @@ -34,13 +36,13 @@ describe('content_editor/services/content_editor', () => { serializer.deserialize.mockResolvedValueOnce(''); }); - it('emits loadingContent and loadingSuccess event', () => { + it('emits loadingContent and loadingSuccess event in the eventHub', () => { let loadingContentEmitted = false; - contentEditor.on(LOADING_CONTENT_EVENT, () => { + eventHub.$on(LOADING_CONTENT_EVENT, () => { loadingContentEmitted = true; }); - contentEditor.on(LOADING_SUCCESS_EVENT, () => { + eventHub.$on(LOADING_SUCCESS_EVENT, () => { expect(loadingContentEmitted).toBe(true); }); @@ -56,7 +58,7 @@ describe('content_editor/services/content_editor', () => { }); it('emits loadingError event', async () => { - contentEditor.on(LOADING_ERROR_EVENT, (e) => { + eventHub.$on(LOADING_ERROR_EVENT, (e) => { expect(e).toBe('error'); }); diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js index 226322a2951..cd3ee734466 100644 --- a/spec/frontend/jobs/components/job_log_controllers_spec.js +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -8,7 +8,6 @@ describe('Job log controllers', () => { afterEach(() => { if (wrapper?.destroy) { wrapper.destroy(); - wrapper = null; } }); @@ -34,7 +33,6 @@ describe('Job log controllers', () => { const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]'); const findRawLink = () => wrapper.find('[data-testid="raw-link"]'); const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]'); - const findEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]'); const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]'); const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]'); @@ -76,28 +74,6 @@ describe('Job log controllers', () => { expect(findRawLinkController().exists()).toBe(false); }); }); - - describe('when is erasable', () => { - beforeEach(() => { - createWrapper(); - }); - - it('renders erase job link', () => { - expect(findEraseLink().exists()).toBe(true); - }); - }); - - describe('when it is not erasable', () => { - beforeEach(() => { - createWrapper({ - erasePath: null, - }); - }); - - it('does not render erase button', () => { - expect(findEraseLink().exists()).toBe(false); - }); - }); }); describe('scroll buttons', () => { diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js index 6914b8d4fa1..ad72b9be261 100644 --- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js +++ b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js @@ -1,5 +1,4 @@ -import { GlButton, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue'; import createStore from '~/jobs/store'; import job from '../mock_data'; @@ -9,12 +8,12 @@ describe('Job Sidebar Retry Button', () => { let wrapper; const forwardDeploymentFailure = 'forward_deployment_failure'; - const findRetryButton = () => wrapper.find(GlButton); - const findRetryLink = () => wrapper.find(GlLink); + const findRetryButton = () => wrapper.findByTestId('retry-job-button'); + const findRetryLink = () => wrapper.findByTestId('retry-job-link'); const createWrapper = ({ props = {} } = {}) => { store = createStore(); - wrapper = shallowMount(JobsSidebarRetryButton, { + wrapper = shallowMountExtended(JobsSidebarRetryButton, { propsData: { href: job.retry_path, modalId: 'modal-id', @@ -27,7 +26,6 @@ describe('Job Sidebar Retry Button', () => { afterEach(() => { if (wrapper) { wrapper.destroy(); - wrapper = null; } }); @@ -44,7 +42,6 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryButton().exists()).toBe(buttonExists); expect(findRetryLink().exists()).toBe(linkExists); - expect(wrapper.text()).toMatch('Retry'); }, ); @@ -55,6 +52,7 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryButton().attributes()).toMatchObject({ category: 'primary', variant: 'confirm', + icon: 'retry', }); }); }); @@ -64,6 +62,7 @@ describe('Job Sidebar Retry Button', () => { expect(findRetryLink().attributes()).toMatchObject({ 'data-method': 'post', href: job.retry_path, + icon: 'retry', }); }); }); diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js index 6e327725627..39c71986ce4 100644 --- a/spec/frontend/jobs/components/sidebar_spec.js +++ b/spec/frontend/jobs/components/sidebar_spec.js @@ -21,25 +21,54 @@ describe('Sidebar details block', () => { const findNewIssueButton = () => wrapper.findByTestId('job-new-issue'); const findRetryButton = () => wrapper.find(JobRetryButton); const findTerminalLink = () => wrapper.findByTestId('terminal-link'); + const findEraseLink = () => wrapper.findByTestId('job-log-erase-link'); - const createWrapper = ({ props = {} } = {}) => { + const createWrapper = (props) => { store = createStore(); store.state.job = job; wrapper = extendedWrapper( shallowMount(Sidebar, { - ...props, + propsData: { + ...props, + }, + store, }), ); }; afterEach(() => { - if (wrapper) { - wrapper.destroy(); - wrapper = null; - } + wrapper.destroy(); + }); + + describe('when job log is erasable', () => { + const path = '/root/ci-project/-/jobs/1447/erase'; + + beforeEach(() => { + createWrapper({ + erasePath: path, + }); + }); + + it('renders erase job link', () => { + expect(findEraseLink().exists()).toBe(true); + }); + + it('erase job link has correct path', () => { + expect(findEraseLink().attributes('href')).toBe(path); + }); + }); + + describe('when job log is not erasable', () => { + beforeEach(() => { + createWrapper(); + }); + + it('does not render erase button', () => { + expect(findEraseLink().exists()).toBe(false); + }); }); describe('when there is no retry path retry', () => { @@ -86,7 +115,7 @@ describe('Sidebar details block', () => { }); it('should render link to cancel job', () => { - expect(findCancelButton().text()).toMatch('Cancel'); + expect(findCancelButton().props('icon')).toBe('cancel'); expect(findCancelButton().attributes('href')).toBe(job.cancel_path); }); }); diff --git a/spec/frontend/pipeline_wizard/components/widgets/list_spec.js b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js new file mode 100644 index 00000000000..796356634bc --- /dev/null +++ b/spec/frontend/pipeline_wizard/components/widgets/list_spec.js @@ -0,0 +1,212 @@ +import { GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import ListWidget from '~/pipeline_wizard/components/widgets/list.vue'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; + +describe('Pipeline Wizard - List Widget', () => { + const defaultProps = { + label: 'This label', + description: 'some description', + placeholder: 'some placeholder', + pattern: '^[a-z]+$', + invalidFeedback: 'some feedback', + }; + let wrapper; + let addStepBtn; + + const findGlFormGroup = () => wrapper.findComponent(GlFormGroup); + const findGlFormGroupInvalidFeedback = () => findGlFormGroup().find('.invalid-feedback').text(); + const findFirstGlFormInputGroup = () => wrapper.findComponent(GlFormInputGroup); + const findAllGlFormInputGroups = () => wrapper.findAllComponents(GlFormInputGroup); + const findGlFormInputGroupByIndex = (index) => findAllGlFormInputGroups().at(index); + const setValueOnInputField = (value, atIndex = 0) => { + return findGlFormInputGroupByIndex(atIndex).vm.$emit('input', value); + }; + const findAddStepButton = () => wrapper.findByTestId('add-step-button'); + const addStep = () => findAddStepButton().vm.$emit('click'); + + const createComponent = (props = {}, mountFn = shallowMountExtended) => { + wrapper = mountFn(ListWidget, { + propsData: { + ...defaultProps, + ...props, + }, + }); + addStepBtn = findAddStepButton(); + }; + + describe('component setup and interface', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('prints the label inside the legend', () => { + createComponent(); + + expect(findGlFormGroup().attributes('label')).toBe(defaultProps.label); + }); + + it('prints the description inside the legend', () => { + createComponent(); + + expect(findGlFormGroup().attributes('labeldescription')).toBe(defaultProps.description); + }); + + it('sets the input field type attribute to "text"', async () => { + createComponent(); + + expect(findFirstGlFormInputGroup().attributes('type')).toBe('text'); + }); + + it('passes the placeholder to the first input field', () => { + createComponent(); + + expect(findFirstGlFormInputGroup().attributes('placeholder')).toBe(defaultProps.placeholder); + }); + + it('shows a delete button on all fields if there are more than one', async () => { + createComponent({}, mountExtended); + + await addStep(); + await addStep(); + const inputGroups = findAllGlFormInputGroups().wrappers; + + expect(inputGroups.length).toBe(3); + inputGroups.forEach((inputGroup) => { + const button = inputGroup.find('[data-testid="remove-step-button"]'); + expect(button.find('[data-testid="remove-icon"]').exists()).toBe(true); + expect(button.attributes('aria-label')).toBe('remove step'); + }); + }); + + it('null values do not cause an input event', async () => { + createComponent(); + + await addStep(); + + expect(wrapper.emitted('input')).toBe(undefined); + }); + + it('hides the delete button if there is only one', () => { + createComponent({}, mountExtended); + + const inputGroups = findAllGlFormInputGroups().wrappers; + + expect(inputGroups.length).toBe(1); + expect(wrapper.findByTestId('remove-step-button').exists()).toBe(false); + }); + + it('shows an "add step" button', () => { + createComponent(); + + expect(addStepBtn.attributes('icon')).toBe('plus'); + expect(addStepBtn.text()).toBe('add another step'); + }); + + it('the "add step" button increases the number of input fields', async () => { + createComponent(); + + expect(findAllGlFormInputGroups().wrappers.length).toBe(1); + await addStep(); + expect(findAllGlFormInputGroups().wrappers.length).toBe(2); + }); + + it('does not pass the placeholder on subsequent input fields', async () => { + createComponent(); + + await addStep(); + await addStep(); + const nullOrUndefined = [null, undefined]; + expect(nullOrUndefined).toContain(findAllGlFormInputGroups().at(1).attributes('placeholder')); + expect(nullOrUndefined).toContain(findAllGlFormInputGroups().at(2).attributes('placeholder')); + }); + + it('emits an update event on input', async () => { + createComponent(); + + const localValue = 'somevalue'; + await setValueOnInputField(localValue); + await nextTick(); + + expect(wrapper.emitted('input')).toEqual([[[localValue]]]); + }); + + it('only emits non-null values', async () => { + createComponent(); + + await addStep(); + await addStep(); + await setValueOnInputField('abc', 1); + await nextTick(); + + const events = wrapper.emitted('input'); + + expect(events.length).toBe(1); + expect(events[0]).toEqual([['abc']]); + }); + }); + + describe('form validation', () => { + afterEach(() => { + wrapper.destroy(); + }); + + it('does not show validation state when untouched', async () => { + createComponent({}, mountExtended); + expect(findGlFormGroup().classes()).not.toContain('is-valid'); + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + }); + + it('shows invalid state on blur', async () => { + createComponent({}, mountExtended); + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + const input = findFirstGlFormInputGroup().find('input'); + await input.setValue('invalid99'); + await input.trigger('blur'); + expect(input.classes()).toContain('is-invalid'); + expect(findGlFormGroup().classes()).toContain('is-invalid'); + }); + + it('shows invalid state when toggling `validate` prop', async () => { + createComponent({ required: true, validate: false }, mountExtended); + await setValueOnInputField(null); + expect(findGlFormGroup().classes()).not.toContain('is-invalid'); + await wrapper.setProps({ validate: true }); + expect(findGlFormGroup().classes()).toContain('is-invalid'); + }); + + it.each` + scenario | required | values | inputFieldClasses | inputGroupClass | feedback + ${'shows invalid if all inputs are empty'} | ${true} | ${[null, null]} | ${['is-invalid', null]} | ${'is-invalid'} | ${'At least one entry is required'} + ${'is valid if at least one field has a valid entry'} | ${true} | ${[null, 'abc']} | ${[null, 'is-valid']} | ${'is-valid'} | ${expect.anything()} + ${'is invalid if one field has an invalid entry'} | ${true} | ${['abc', '99']} | ${['is-valid', 'is-invalid']} | ${'is-invalid'} | ${defaultProps.invalidFeedback} + ${'is not invalid if its not required but all values are null'} | ${false} | ${[null, null]} | ${[null, null]} | ${'is-valid'} | ${expect.anything()} + ${'is invalid if pattern does not match even if its not required'} | ${false} | ${['99', null]} | ${['is-invalid', null]} | ${'is-invalid'} | ${defaultProps.invalidFeedback} + `('$scenario', async ({ required, values, inputFieldClasses, inputGroupClass, feedback }) => { + createComponent({ required, validate: true }, mountExtended); + + await Promise.all( + values.map(async (value, i) => { + if (i > 0) { + await addStep(); + } + await setValueOnInputField(value, i); + }), + ); + await nextTick(); + + inputFieldClasses.forEach((expected, i) => { + const inputWrapper = findGlFormInputGroupByIndex(i).find('input'); + if (expected === null) { + expect(inputWrapper.classes()).not.toContain('is-valid'); + expect(inputWrapper.classes()).not.toContain('is-invalid'); + } else { + expect(inputWrapper.classes()).toContain(expected); + } + }); + + expect(findGlFormGroup().classes()).toContain(inputGroupClass); + expect(findGlFormGroupInvalidFeedback()).toEqual(feedback); + }); + }); +}); diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index b1a04f0592a..9040731d8fd 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -32,12 +32,21 @@ RSpec.describe Gitlab::Email::Receiver do metadata = receiver.mail_metadata - expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta)) + expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients)) expect(metadata[:meta]).to include(client_id: 'email/jake@example.com', project: project.full_path) expect(metadata[meta_key]).to eq(meta_value) end end + shared_examples 'failed receive' do + it 'adds metric event' do + expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction) + expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name }) + + expect { receiver.execute }.to raise_error(expected_error) + end + end + context 'when the email contains a valid email address in a header' do before do stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com") @@ -74,14 +83,25 @@ RSpec.describe Gitlab::Email::Receiver do it_behaves_like 'successful receive' end - end - shared_examples 'failed receive' do - it 'adds metric event' do - expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction) - expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name }) + context 'when all other headers are missing' do + let(:email_raw) { fixture_file('emails/missing_delivered_to_header.eml') } + let(:meta_key) { :received_recipients } + let(:meta_value) { ['incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com', 'incoming+gitlabhq/gitlabhq@example.com'] } - expect { receiver.execute }.to raise_error(expected_error) + context 'when use_received_header_for_incoming_emails is enabled' do + it_behaves_like 'successful receive' + end + + context 'when use_received_header_for_incoming_emails is disabled' do + let(:expected_error) { Gitlab::Email::UnknownIncomingEmail } + + before do + stub_feature_flags(use_received_header_for_incoming_emails: false) + end + + it_behaves_like 'failed receive' + end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 565a794b902..72da2c22f29 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -385,23 +385,43 @@ RSpec.describe Group do end end - before do - subject - reload_models(old_parent, new_parent, group) - end - context 'within the same hierarchy' do let!(:root) { create(:group).reload } let!(:old_parent) { create(:group, parent: root) } let!(:new_parent) { create(:group, parent: root) } - it 'updates traversal_ids' do - expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id] + context 'with FOR UPDATE lock' do + before do + stub_feature_flags(for_no_key_update_lock: false) + subject + reload_models(old_parent, new_parent, group) + end + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id] + end + + it_behaves_like 'hierarchy with traversal_ids' + it_behaves_like 'locked row', 'FOR UPDATE' do + let(:row) { root } + end end - it_behaves_like 'hierarchy with traversal_ids' - it_behaves_like 'locked row' do - let(:row) { root } + context 'with FOR NO KEY UPDATE lock' do + before do + stub_feature_flags(for_no_key_update_lock: true) + subject + reload_models(old_parent, new_parent, group) + end + + it 'updates traversal_ids' do + expect(group.traversal_ids).to eq [root.id, new_parent.id, group.id] + end + + it_behaves_like 'hierarchy with traversal_ids' + it_behaves_like 'locked row', 'FOR NO KEY UPDATE' do + let(:row) { root } + end end end @@ -410,6 +430,11 @@ RSpec.describe Group do let!(:new_parent) { create(:group) } let!(:group) { create(:group, parent: old_parent) } + before do + subject + reload_models(old_parent, new_parent, group) + end + it 'updates traversal_ids' do expect(group.traversal_ids).to eq [new_parent.id, group.id] end @@ -435,6 +460,11 @@ RSpec.describe Group do let!(:old_parent) { nil } let!(:new_parent) { create(:group) } + before do + subject + reload_models(old_parent, new_parent, group) + end + it 'updates traversal_ids' do expect(group.traversal_ids).to eq [new_parent.id, group.id] end @@ -452,6 +482,11 @@ RSpec.describe Group do let!(:old_parent) { create(:group) } let!(:new_parent) { nil } + before do + subject + reload_models(old_parent, new_parent, group) + end + it 'updates traversal_ids' do expect(group.traversal_ids).to eq [group.id] end diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb index 51932ab943c..eeea071d326 100644 --- a/spec/models/namespace/traversal_hierarchy_spec.rb +++ b/spec/models/namespace/traversal_hierarchy_spec.rb @@ -68,11 +68,24 @@ RSpec.describe Namespace::TraversalHierarchy, type: :model do end end - it_behaves_like 'locked row' do + it_behaves_like 'locked row', 'FOR UPDATE' do let(:recorded_queries) { ActiveRecord::QueryRecorder.new } let(:row) { root } before do + stub_feature_flags(for_no_key_update_lock: false) + + recorded_queries.record { subject } + end + end + + it_behaves_like 'locked row', 'FOR NO KEY UPDATE' do + let(:recorded_queries) { ActiveRecord::QueryRecorder.new } + let(:row) { root } + + before do + stub_feature_flags(for_no_key_update_lock: true) + recorded_queries.record { subject } end end diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb index 573da862b57..771bab20b75 100644 --- a/spec/requests/api/error_tracking/collector_spec.rb +++ b/spec/requests/api/error_tracking/collector_spec.rb @@ -171,6 +171,12 @@ RSpec.describe API::ErrorTracking::Collector do it_behaves_like 'successful request' end + context 'when JSON key transaction is empty string' do + let_it_be(:raw_event) { fixture_file('error_tracking/php_empty_transaction.json') } + + it_behaves_like 'successful request' + end + context 'sentry_key as param and empty headers' do let(:url) { "/error_tracking/collector/api/#{project.id}/store?sentry_key=#{sentry_key}" } let(:headers) { {} } diff --git a/spec/services/error_tracking/collect_error_service_spec.rb b/spec/services/error_tracking/collect_error_service_spec.rb index 2b16612dac3..faca3c12a48 100644 --- a/spec/services/error_tracking/collect_error_service_spec.rb +++ b/spec/services/error_tracking/collect_error_service_spec.rb @@ -51,25 +51,30 @@ RSpec.describe ErrorTracking::CollectErrorService do end end - context 'unusual payload' do + context 'with unusual payload' do let(:modified_event) { parsed_event } + let(:event) { described_class.new(project, nil, event: modified_event).execute } - context 'missing transaction' do + context 'when transaction is missing' do it 'builds actor from stacktrace' do modified_event.delete('transaction') - event = described_class.new(project, nil, event: modified_event).execute + expect(event.error.actor).to eq 'find()' + end + end + + context 'when transaction is an empty string' do \ + it 'builds actor from stacktrace' do + modified_event['transaction'] = '' expect(event.error.actor).to eq 'find()' end end - context 'timestamp is numeric' do + context 'when timestamp is numeric' do it 'parses timestamp' do modified_event['timestamp'] = '1631015580.50' - event = described_class.new(project, nil, event: modified_event).execute - expect(event.occurred_at).to eq '2021-09-07T11:53:00.5' end end diff --git a/spec/support/shared_examples/row_lock_shared_examples.rb b/spec/support/shared_examples/row_lock_shared_examples.rb index 5e003172215..e7eec88ec42 100644 --- a/spec/support/shared_examples/row_lock_shared_examples.rb +++ b/spec/support/shared_examples/row_lock_shared_examples.rb @@ -4,10 +4,10 @@ # Ensure a transaction also occurred. # Be careful! This form of spec is not foolproof, but better than nothing. -RSpec.shared_examples 'locked row' do +RSpec.shared_examples 'locked row' do |lock_type| it "has locked row" do table_name = row.class.table_name - ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+FOR UPDATE/m + ids_regex = /SELECT.*FROM.*#{table_name}.*"#{table_name}"."id" = #{row.id}.+#{lock_type}/m expect(recorded_queries.log).to include a_string_matching 'SAVEPOINT' expect(recorded_queries.log).to include a_string_matching ids_regex |