Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-02-24 18:15:02 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-02-24 18:15:02 +0300
commitc4b4a75c35cb2015c01ef0b60f8ad8baaaf889df (patch)
tree16eabfd63477e1904d7eb5d9f92f3e5a4e4d3e0f /spec
parente40c68997d44209aed2baf3a8ec6be9ae99fb0b5 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/jobs_spec.rb6
-rw-r--r--spec/fixtures/emails/missing_delivered_to_header.eml35
-rw-r--r--spec/fixtures/emails/valid_note_on_issuable.eml2
-rw-r--r--spec/fixtures/error_tracking/php_empty_transaction.json45
-rw-r--r--spec/frontend/branches/ajax_loading_spinner_spec.js32
-rw-r--r--spec/frontend/content_editor/components/content_editor_alert_spec.js17
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js10
-rw-r--r--spec/frontend/content_editor/components/editor_state_observer_spec.js63
-rw-r--r--spec/frontend/content_editor/components/toolbar_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_link_button_spec.js2
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js14
-rw-r--r--spec/frontend/content_editor/services/content_editor_spec.js14
-rw-r--r--spec/frontend/jobs/components/job_log_controllers_spec.js24
-rw-r--r--spec/frontend/jobs/components/job_sidebar_retry_button_spec.js13
-rw-r--r--spec/frontend/jobs/components/sidebar_spec.js43
-rw-r--r--spec/frontend/pipeline_wizard/components/widgets/list_spec.js212
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb34
-rw-r--r--spec/models/group_spec.rb55
-rw-r--r--spec/models/namespace/traversal_hierarchy_spec.rb15
-rw-r--r--spec/requests/api/error_tracking/collector_spec.rb6
-rw-r--r--spec/services/error_tracking/collect_error_service_spec.rb17
-rw-r--r--spec/support/shared_examples/row_lock_shared_examples.rb4
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