diff options
Diffstat (limited to 'spec')
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:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: long hex encoding'} | ${'javascript:'} | ${null} + ${'protocol-based JS injection: hex encoding without semicolons'} | ${'javascript:alert('XSS')'} | ${null} + ${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null} + ${'protocol-based JS injection: spaces and entities'} | ${" 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] |