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-08-12 18:11:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-12 18:11:42 +0300
commit1c8734ca5c2981e62b9c1162851ed136de86bbbf (patch)
treefbe5f20188dc94df75ee051c0371c3bed1f7ce63 /spec
parentee772e0c77e11099a9f077f780cd1a18de803222 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/invites_spec.rb22
-rw-r--r--spec/features/oauth_registration_spec.rb39
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js12
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js8
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js92
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js82
-rw-r--r--spec/frontend/issues/show/components/app_spec.js4
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js2
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js2
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js2
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js48
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js34
-rw-r--r--spec/graphql/types/projects/service_type_enum_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb123
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/security/report_schema_version_matcher_spec.rb44
-rw-r--r--spec/models/integrations/pumble_spec.rb14
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/requests/api/integrations_spec.rb1
20 files changed, 407 insertions, 127 deletions
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index fe804dc52d7..1baa97096d9 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -182,12 +182,14 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
- it 'signs up and redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
- fill_in_sign_up_form(new_user)
- fill_in_welcome_form
+ context 'the user signs up for an account with the invitation email address' do
+ it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
+ fill_in_sign_up_form(new_user)
+ fill_in_welcome_form
- expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
- expect(page).to have_content('You have been granted Owner access to group Owned.')
+ expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
+ expect(page).to have_content('You have been granted Owner access to group Owned.')
+ end
end
context 'the user sign-up using a different email address' do
@@ -227,11 +229,13 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end
end
- it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do
- fill_in_sign_up_form(new_user)
- fill_in_welcome_form
+ context 'the user signs up for an account with the invitation email address' do
+ it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
+ fill_in_sign_up_form(new_user)
+ fill_in_welcome_form
- expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
+ expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
+ end
end
context 'the user sign-up using a different email address' do
diff --git a/spec/features/oauth_registration_spec.rb b/spec/features/oauth_registration_spec.rb
index 18dd10755b1..cb8343b8065 100644
--- a/spec/features/oauth_registration_spec.rb
+++ b/spec/features/oauth_registration_spec.rb
@@ -85,7 +85,46 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection do
expect(page).to have_content('Please complete your profile with email address')
end
end
+
+ context 'when registering via an invitation email' do
+ let_it_be(:owner) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'Owned') }
+ let_it_be(:project) { create(:project, :repository, namespace: group) }
+
+ let(:invite_email) { generate(:email) }
+ let(:extra_params) { { invite_type: Emails::Members::INITIAL_INVITE } }
+ let(:group_invite) do
+ create(
+ :group_member, :invited,
+ group: group,
+ invite_email: invite_email,
+ created_by: owner
+ )
+ end
+
+ before do
+ project.add_maintainer(owner)
+ group.add_owner(owner)
+ group_invite.generate_invite_token!
+
+ mock_auth_hash(provider, uid, invite_email, additional_info: additional_info)
+ end
+
+ it 'redirects to the activity page with all the projects/groups invitations accepted' do
+ visit invite_path(group_invite.raw_invite_token, extra_params)
+ click_link_or_button "oauth-login-#{provider}"
+ fill_in_welcome_form
+
+ expect(page).to have_content('You have been granted Owner access to group Owned.')
+ expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
+ end
+ end
end
end
end
+
+ def fill_in_welcome_form
+ select 'Software Developer', from: 'user_role'
+ click_button 'Get started!'
+ end
end
diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
index a5007e18f5f..70ed9eeb3e1 100644
--- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
+++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js
@@ -87,8 +87,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
- expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
+ expect(findPrimaryButton().attributes('disabled')).toBe('true');
+ expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@@ -105,8 +105,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
- expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
+ expect(findPrimaryButton().attributes('disabled')).toBe('true');
+ expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@@ -123,8 +123,8 @@ describe('Delete user modal', () => {
});
it('has enabled buttons', () => {
- expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
- expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
+ expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
+ expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
});
describe('when primary action is clicked', () => {
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
index 42c6501dcce..6681ab91a4a 100644
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
@@ -58,7 +58,7 @@ describe('Ci variable modal', () => {
});
it('button is disabled when no key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
});
});
@@ -71,7 +71,7 @@ describe('Ci variable modal', () => {
});
it('button is enabled when key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
it('Add variable button dispatches addVariable action', () => {
@@ -249,7 +249,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
});
it('shows the correct error text', () => {
@@ -316,7 +316,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index ca552644258..0f9073dc26d 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -261,7 +261,7 @@ describe('Client side Markdown processing', () => {
...source('<img src="bar" alt="foo" />'),
alt: 'foo',
canonicalSrc: 'bar',
- src: 'http://test.host/bar',
+ src: 'bar',
}),
),
),
@@ -283,7 +283,7 @@ describe('Client side Markdown processing', () => {
image({
...source('<img src="bar" alt="foo" />'),
alt: 'foo',
- src: 'http://test.host/bar',
+ src: 'bar',
canonicalSrc: 'bar',
}),
),
@@ -297,7 +297,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
- href: 'https://gitlab.com/',
+ href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
@@ -316,7 +316,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
- href: 'https://gitlab.com/',
+ href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
@@ -335,7 +335,7 @@ describe('Client side Markdown processing', () => {
{
...source('www.commonmark.org'),
canonicalSrc: 'http://www.commonmark.org',
- href: 'http://www.commonmark.org/',
+ href: 'http://www.commonmark.org',
},
'www.commonmark.org',
),
@@ -389,7 +389,7 @@ describe('Client side Markdown processing', () => {
sourceMapKey: null,
sourceMarkdown: null,
canonicalSrc: 'https://gitlab.com',
- href: 'https://gitlab.com/',
+ href: 'https://gitlab.com',
},
'https://gitlab.com',
),
@@ -616,7 +616,7 @@ two
...source('![bar](foo.png)'),
alt: 'bar',
canonicalSrc: 'foo.png',
- src: 'http://test.host/foo.png',
+ src: 'foo.png',
}),
),
),
@@ -969,12 +969,12 @@ Paragraph
{
...source('[![moon](moon.jpg)](/uri)'),
canonicalSrc: '/uri',
- href: 'http://test.host/uri',
+ href: '/uri',
},
image({
...source('![moon](moon.jpg)'),
canonicalSrc: 'moon.jpg',
- src: 'http://test.host/moon.jpg',
+ src: 'moon.jpg',
alt: 'moon',
}),
),
@@ -1010,7 +1010,7 @@ Paragraph
{
...source('[moon](moon.jpg)'),
canonicalSrc: 'moon.jpg',
- href: 'http://test.host/moon.jpg',
+ href: 'moon.jpg',
},
'moon',
),
@@ -1021,7 +1021,7 @@ Paragraph
link(
{
...source('[sun](sun.jpg)'),
- href: 'http://test.host/sun.jpg',
+ href: 'sun.jpg',
canonicalSrc: 'sun.jpg',
},
'sun',
@@ -1141,7 +1141,7 @@ _world_.
link(
{
...source('[GitLab][gitlab-url]'),
- href: 'https://gitlab.com/',
+ href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'GitLab',
},
@@ -1235,4 +1235,72 @@ body {
expect(tiptapEditor.getHTML()).toEqual(expectedHtml);
},
);
+
+ describe('attribute sanitization', () => {
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');";
+ // eslint-disable-next-line no-script-url
+ const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');";
+
+ const docWithImageFactory = (urlInput, urlOutput) => {
+ const input = `<img src="${urlInput}">`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ image({
+ ...source(input),
+ src: urlOutput,
+ canonicalSrc: urlOutput,
+ }),
+ ),
+ ),
+ };
+ };
+
+ const docWithLinkFactory = (urlInput, urlOutput) => {
+ const input = `<a href="${urlInput}">foo</a>`;
+
+ return {
+ input,
+ expectedDoc: doc(
+ paragraph(
+ source(input),
+ link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'),
+ ),
+ ),
+ };
+ };
+
+ it.each`
+ desc | urlInput | urlOutput
+ ${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
+ ${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null}
+ ${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null}
+ ${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript&#58;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript&#0058;'} | ${null}
+ ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041'} | ${null}
+ ${'protocol-based JS injection: hex encoding'} | ${'javascript&#x3A;'} | ${null}
+ ${'protocol-based JS injection: long hex encoding'} | ${'javascript&#x003A;'} | ${null}
+ ${'protocol-based JS injection: hex encoding without semicolons'} | ${'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29'} | ${null}
+ ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null}
+ ${'protocol-based JS injection: spaces and entities'} | ${"&#14; javascript:alert('XSS');"} | ${null}
+ ${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null}
+ ${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
+ ${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"}
+ ${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
+ `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
+ const exampleFactories = [docWithImageFactory, docWithLinkFactory];
+
+ exampleFactories.forEach(async (exampleFactory) => {
+ const { input, expectedDoc } = exampleFactory(urlInput, urlOutput);
+ const document = await deserialize(input);
+
+ expect(document.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 55f17727df7..37fe2482123 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -1213,47 +1213,47 @@ paragraph
};
it.each`
- mark | markdown | modifiedMarkdown | editAction
- ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
- ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
- ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
- ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
- ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
- ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
- ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
- ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
- ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
- ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com/">link modified</a>'} | ${defaultEditAction}
- ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
- ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
- ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
- ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
- ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
- ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
- ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
- ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
- ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
- ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
- ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
- ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
- ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
- ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
- ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
- ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
- ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
- ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
- ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
+ mark | markdown | modifiedMarkdown | editAction
+ ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
+ ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
+ ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
+ ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
+ ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
+ ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
+ ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
+ ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
+ ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
+ ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
+ ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
+ ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
+ ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction}
+ ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
+ ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
+ ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
+ ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
+ ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
+ ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
+ ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
+ ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
+ ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
+ ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
+ ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
+ ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
`(
'preserves original $mark syntax when sourceMarkdown is available for $markdown',
async ({ markdown, modifiedMarkdown, editAction }) => {
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 27604b8ccf3..12f9707da04 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -119,7 +119,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
- expect(findEdited().props('updatedAt')).toBeTruthy();
+ expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
})
.then(() => {
@@ -133,7 +133,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByName')).toBe('Other User');
expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
- expect(findEdited().props('updatedAt')).toBeTruthy();
+ expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
});
});
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 136a5967ee4..b0218a9df12 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -148,7 +148,7 @@ describe('ProjectDropdown', () => {
});
it('emits `error` event', () => {
- expect(wrapper.emitted('error')).toBeTruthy();
+ expect(wrapper.emitted('error')).toHaveLength(1);
});
});
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 70df05a2781..6cfbdb16111 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
});
it('clicked on link with view', () => {
- expect(primaryLink.props('menuItem').view).toBeTruthy();
+ expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace);
});
it('changes active view', () => {
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 3350609bb90..59e2f15faa4 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -357,7 +357,7 @@ describe('issue_note', () => {
createWrapper();
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
- expect(wrapper.emitted('handleUpdateNote')).toBeTruthy();
+ expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
it('does not stringify empty position', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index 6c743f92116..f958f12acd4 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => {
it('emits filterPipelines on submit with correct filter', () => {
findFilteredSearch().vm.$emit('submit', mockSearch);
- expect(wrapper.emitted('filterPipelines')).toBeTruthy();
+ expect(wrapper.emitted('filterPipelines')).toHaveLength(1);
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index fc906194059..a079b0b97fd 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
-
- expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
+
+ expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show empty state when called with `empty`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false);
@@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li');
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
- expect(
- customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
- ).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
expect($metricsListLi.length).toEqual(metrics.length);
});
@@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => {
false,
);
- expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
- expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
+ expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
+ expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
});
});
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index 0df2aad5882..a65cbe1a47a 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => {
it('should show loading state when called with `loading`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
});
it('should show empty state when called with `empty`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
});
@@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
'3 exporters with 12 metrics were found',
@@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => {
it('should show missing environment variables list', () => {
prometheusMetrics.populateActiveMetrics(missingVarMetrics);
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
@@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => {
prometheusMetrics.loadActiveMetrics();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
await waitForPromises();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
});
it('should show empty state if response failed to load', async () => {
@@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => {
await waitForPromises();
- expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
- expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
+ expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
+ expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
});
it('should populate metrics list once response is loaded', async () => {
diff --git a/spec/graphql/types/projects/service_type_enum_spec.rb b/spec/graphql/types/projects/service_type_enum_spec.rb
index ead69e60f6c..f7256910bb0 100644
--- a/spec/graphql/types/projects/service_type_enum_spec.rb
+++ b/spec/graphql/types/projects/service_type_enum_spec.rb
@@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['ServiceType'] do
PIPELINES_EMAIL_SERVICE
PIVOTALTRACKER_SERVICE
PROMETHEUS_SERVICE
+ PUMBLE_SERVICE
PUSHOVER_SERVICE
REDMINE_SERVICE
SHIMO_SERVICE
diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
index aaac75e072f..77611c98179 100644
--- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
@@ -68,6 +68,49 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
describe '#valid?' do
subject { validator.valid? }
+ context 'when given a supported MAJOR.MINOR schema version' do
+ let(:report_type) { :dast }
+ let(:report_version) do
+ latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
+ (latest_vendored_version[0...2] << "34").join(".")
+ end
+
+ context 'and the report is valid' do
+ let(:report_data) do
+ {
+ 'version' => report_version,
+ 'vulnerabilities' => []
+ }
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'and the report is invalid' do
+ let(:report_data) do
+ {
+ 'version' => report_version
+ }
+ end
+
+ it { is_expected.to be_falsey }
+
+ it 'logs related information' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ message: "security report schema validation problem",
+ security_report_type: report_type,
+ security_report_version: report_version,
+ project_id: project.id,
+ security_report_failure: 'schema_validation_fails',
+ security_report_scanner_id: 'gemnasium',
+ security_report_scanner_version: '2.1.0'
+ )
+
+ subject
+ end
+ end
+ end
+
context 'when given a supported schema version' do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
@@ -320,6 +363,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
context 'when given an unsupported schema version' do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
+ let(:expected_unsupported_message) do
+ "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\
+ "#{supported_dast_versions}. GitLab will attempt to validate this report against the earliest supported "\
+ "versions of this report type, to show all the errors but will not ingest the report"
+ end
context 'and the report is valid' do
let(:report_data) do
@@ -331,7 +379,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}"
+ expected_unsupported_message
]
end
@@ -347,7 +395,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}",
+ expected_unsupported_message,
"root is missing required keys: vulnerabilities"
]
end
@@ -359,6 +407,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
context 'when not given a schema version' do
let(:report_type) { :dast }
let(:report_version) { nil }
+ let(:expected_missing_version_message) do
+ "Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\
+ "will attempt to validate this report against the earliest supported versions of this report type, to show all "\
+ "the errors but will not ingest the report"
+ end
+
let(:report_data) do
{
'vulnerabilities' => []
@@ -368,7 +422,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
"root is missing required keys: version",
- "Report version not provided, dast report type supports versions: #{supported_dast_versions}"
+ expected_missing_version_message
]
end
@@ -414,9 +468,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last }
+ let(:expected_deprecation_message) do
+ "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this "\
+ "report type are: #{supported_dast_versions}. GitLab will attempt to parse and ingest this report if valid."
+ end
+
let(:expected_deprecation_warnings) do
[
- "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}"
+ expected_deprecation_message
]
end
@@ -464,6 +523,62 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
describe '#warnings' do
subject { validator.warnings }
+ context 'when given a supported MAJOR.MINOR schema version' do
+ let(:report_type) { :dast }
+ let(:report_version) do
+ latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
+ (latest_vendored_version[0...2] << "34").join(".")
+ end
+
+ let(:latest_patch_version) do
+ ::Security::ReportSchemaVersionMatcher.new(
+ report_declared_version: report_version,
+ supported_versions: described_class::SUPPORTED_VERSIONS[report_type]
+ ).call
+ end
+
+ let(:message) do
+ "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
+ " any vendored schema version. Validation will be attempted against version"\
+ " #{latest_patch_version}"
+ end
+
+ context 'and the report is valid' do
+ let(:report_data) do
+ {
+ 'version' => report_version,
+ 'vulnerabilities' => []
+ }
+ end
+
+ it { is_expected.to match_array([message]) }
+ end
+
+ context 'and the report is invalid' do
+ let(:report_data) do
+ {
+ 'version' => report_version
+ }
+ end
+
+ it { is_expected.to match_array([message]) }
+
+ it 'logs related information' do
+ expect(Gitlab::AppLogger).to receive(:info).with(
+ message: "security report schema validation problem",
+ security_report_type: report_type,
+ security_report_version: report_version,
+ project_id: project.id,
+ security_report_failure: 'schema_validation_fails',
+ security_report_scanner_id: 'gemnasium',
+ security_report_scanner_version: '2.1.0'
+ )
+
+ subject
+ end
+ end
+ end
+
context 'when given a supported schema version' do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 0d03f30a6d2..1a270fb8523 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -597,6 +597,7 @@ project:
- alert_management_alerts
- repository_storage_moves
- freeze_periods
+- pumble_integration
- webex_teams_integration
- build_report_results
- vulnerability_statistic
diff --git a/spec/lib/security/report_schema_version_matcher_spec.rb b/spec/lib/security/report_schema_version_matcher_spec.rb
new file mode 100644
index 00000000000..9c40f0bc6fa
--- /dev/null
+++ b/spec/lib/security/report_schema_version_matcher_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Security::ReportSchemaVersionMatcher do
+ let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] }
+ let(:version_finder) do
+ described_class.new(
+ report_declared_version: report_version,
+ supported_versions: vendored_versions
+ )
+ end
+
+ describe '#call' do
+ subject { version_finder.call }
+
+ context 'when minor version matches' do
+ context 'and report schema patch version does not match any vendored schema versions' do
+ context 'and report version is 14.1.1' do
+ let(:report_version) { '14.1.1' }
+
+ it 'returns 14.1.0' do
+ expect(subject).to eq('14.1.0')
+ end
+ end
+
+ context 'and report version is 14.0.32' do
+ let(:report_version) { '14.0.32' }
+
+ it 'returns 14.0.2' do
+ expect(subject).to eq('14.0.2')
+ end
+ end
+ end
+ end
+
+ context 'when report minor version does not match' do
+ let(:report_version) { '14.2.1' }
+
+ it 'does not return a version' do
+ expect(subject).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/pumble_spec.rb b/spec/models/integrations/pumble_spec.rb
new file mode 100644
index 00000000000..8b9b5d214c6
--- /dev/null
+++ b/spec/models/integrations/pumble_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Integrations::Pumble do
+ it_behaves_like "chat integration", "Pumble" do
+ let(:client_arguments) { webhook_url }
+ let(:payload) do
+ {
+ text: be_present
+ }
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 05651f7d85a..e2911f2201e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -44,6 +44,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:mattermost_integration) }
it { is_expected.to have_one(:hangouts_chat_integration) }
it { is_expected.to have_one(:unify_circuit_integration) }
+ it { is_expected.to have_one(:pumble_integration) }
it { is_expected.to have_one(:webex_teams_integration) }
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }
diff --git a/spec/requests/api/integrations_spec.rb b/spec/requests/api/integrations_spec.rb
index b2db7f7caef..1e8061f9606 100644
--- a/spec/requests/api/integrations_spec.rb
+++ b/spec/requests/api/integrations_spec.rb
@@ -66,6 +66,7 @@ RSpec.describe API::Integrations do
mattermost: %i[deployment_channel labels_to_be_notified],
mock_ci: %i[enable_ssl_verification],
prometheus: %i[manual_configuration],
+ pumble: %i[branches_to_be_notified notify_only_broken_pipelines],
slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified],
unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines],
webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines]