diff options
author | Dimitris <104737204+dimitris-kavvathas-sonarsource@users.noreply.github.com> | 2023-06-15 15:49:19 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-03 20:03:25 +0000 |
commit | a7431823ca6954ea04fc663e6dcb6cd9e55cc11d (patch) | |
tree | 5ea137fb1992d53e946f334929d337b87146fbe6 /server/sonar-web/src/main/js/apps | |
parent | 7101b666dc952fce6dedece3a515d495adfdc84c (diff) | |
download | sonarqube-a7431823ca6954ea04fc663e6dcb6cd9e55cc11d.tar.gz sonarqube-a7431823ca6954ea04fc663e6dcb6cd9e55cc11d.zip |
SONAR-18835 Make webhook secret private (#8479)
Co-authored-by: Ambroise C <ambroise.christea@sonarsource.com>
(cherry picked from commit 441a87e76b1fab056a23b1773364063ab50e6978)
Diffstat (limited to 'server/sonar-web/src/main/js/apps')
23 files changed, 428 insertions, 167 deletions
diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx index 6e88a09c912..3c02424500c 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/App.tsx @@ -24,7 +24,7 @@ import withComponentContext from '../../../app/components/componentContext/withC import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { translate } from '../../../helpers/l10n'; import { Component } from '../../../types/types'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse } from '../../../types/webhook'; import PageActions from './PageActions'; import PageHeader from './PageHeader'; import WebhooksList from './WebhooksList'; @@ -36,7 +36,7 @@ interface Props { interface State { loading: boolean; - webhooks: Webhook[]; + webhooks: WebhookResponse[]; } export class App extends React.PureComponent<Props, State> { @@ -99,19 +99,24 @@ export class App extends React.PureComponent<Props, State> { }; handleUpdate = (data: { webhook: string; name: string; secret?: string; url: string }) => { - const udpateData = { + const updateData = { webhook: data.webhook, name: data.name, url: data.url, - ...(data.secret && { secret: data.secret }), + secret: data.secret, }; - return updateWebhook(udpateData).then(() => { + return updateWebhook(updateData).then(() => { if (this.mounted) { this.setState(({ webhooks }) => ({ webhooks: webhooks.map((webhook) => webhook.key === data.webhook - ? { ...webhook, name: data.name, secret: data.secret, url: data.url } + ? { + ...webhook, + name: data.name, + hasSecret: data.secret === undefined ? webhook.hasSecret : Boolean(data.secret), + url: data.url, + } : webhook ), })); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx index 5d1833ac179..602fdea8986 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx @@ -24,18 +24,13 @@ import ValidationModal from '../../../components/controls/ValidationModal'; import MandatoryFieldMarker from '../../../components/ui/MandatoryFieldMarker'; import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsExplanation'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookBasePayload, WebhookResponse } from '../../../types/webhook'; +import UpdateWebhookSecretField from './UpdateWebhookSecretField'; interface Props { onClose: () => void; - onDone: (data: Values) => Promise<void>; - webhook?: Webhook; -} - -interface Values { - name: string; - secret: string; - url: string; + onDone: (data: WebhookBasePayload) => Promise<void>; + webhook?: WebhookResponse; } export default class CreateWebhookForm extends React.PureComponent<Props> { @@ -45,7 +40,7 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { this.props.onClose(); }; - handleValidate = (data: Values) => { + handleValidate = (data: WebhookBasePayload) => { const { name, secret, url } = data; const errors: { name?: string; secret?: string; url?: string } = {}; if (!name.trim()) { @@ -74,9 +69,9 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { confirmButtonText={confirmButtonText} header={modalHeader} initialValues={{ - name: (webhook && webhook.name) || '', - secret: (webhook && webhook.secret) || '', - url: (webhook && webhook.url) || '', + name: webhook?.name ?? '', + url: webhook?.url ?? '', + secret: isUpdate ? undefined : '', }} onClose={this.props.onClose} onSubmit={this.props.onDone} @@ -125,12 +120,15 @@ export default class CreateWebhookForm extends React.PureComponent<Props> { type="text" value={values.url} /> - <InputValidationField - description={translate('webhooks.secret.description')} + <UpdateWebhookSecretField + description={`${translate('webhooks.secret.description')}${ + isUpdate ? ` ${translate('webhooks.secret.description.update')}` : '' + }`} dirty={dirty} disabled={isSubmitting} error={errors.secret} id="webhook-secret" + isUpdateForm={isUpdate} label={<label htmlFor="webhook-secret">{translate('webhooks.secret')}</label>} name="secret" onBlur={handleBlur} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx index 6ec97f173ce..238ec10c8b9 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeleteWebhookForm.tsx @@ -18,16 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import SimpleModal from '../../../components/controls/SimpleModal'; +import { ResetButtonLink, SubmitButton } from '../../../components/controls/buttons'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse } from '../../../types/webhook'; interface Props { onClose: () => void; onSubmit: () => Promise<void>; - webhook: Webhook; + webhook: WebhookResponse; } export default function DeleteWebhookForm({ onClose, onSubmit, webhook }: Props) { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx index 9dfad0d5b71..85e70128724 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx @@ -19,18 +19,18 @@ */ import * as React from 'react'; import { searchDeliveries } from '../../../api/webhooks'; -import { ResetButtonLink } from '../../../components/controls/buttons'; import ListFooter from '../../../components/controls/ListFooter'; import Modal from '../../../components/controls/Modal'; +import { ResetButtonLink } from '../../../components/controls/buttons'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Paging } from '../../../types/types'; -import { Webhook, WebhookDelivery } from '../../../types/webhook'; +import { WebhookDelivery, WebhookResponse } from '../../../types/webhook'; import DeliveryAccordion from './DeliveryAccordion'; interface Props { onClose: () => void; - webhook: Webhook; + webhook: WebhookResponse; } interface State { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx index d4dba71fc42..ceedc15589c 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/LatestDeliveryForm.tsx @@ -19,16 +19,16 @@ */ import * as React from 'react'; import { getDelivery } from '../../../api/webhooks'; -import { ResetButtonLink } from '../../../components/controls/buttons'; import Modal from '../../../components/controls/Modal'; +import { ResetButtonLink } from '../../../components/controls/buttons'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { Webhook, WebhookDelivery } from '../../../types/webhook'; +import { WebhookDelivery, WebhookResponse } from '../../../types/webhook'; import DeliveryItem from './DeliveryItem'; interface Props { delivery: WebhookDelivery; onClose: () => void; - webhook: Webhook; + webhook: WebhookResponse; } interface State { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx new file mode 100644 index 00000000000..6c01c7bf683 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/UpdateWebhookSecretField.tsx @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { useField } from 'formik'; +import * as React from 'react'; +import InputValidationField from '../../../components/controls/InputValidationField'; +import ModalValidationField from '../../../components/controls/ModalValidationField'; +import { ButtonLink } from '../../../components/controls/buttons'; +import { translate } from '../../../helpers/l10n'; + +interface Props { + description?: string; + dirty: boolean; + disabled: boolean; + error: string | undefined; + id?: string; + isUpdateForm: boolean; + label?: React.ReactNode; + name: string; + onBlur: (event: React.FocusEvent<HTMLInputElement>) => void; + onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; + touched: boolean | undefined; + type?: string; + value?: string; +} + +export default function UpdateWebhookSecretField({ + isUpdateForm, + description, + dirty, + disabled, + error, + id, + label, + name, + onBlur, + onChange, + touched, + type, + value, +}: Props) { + const [isSecretInputDisplayed, setIsSecretInputDisplayed] = React.useState(false); + const [, , { setValue: setSecretValue }] = useField('secret'); + + const showSecretInput = () => { + setSecretValue(''); + setIsSecretInputDisplayed(true); + }; + + return !isUpdateForm || isSecretInputDisplayed ? ( + <InputValidationField + description={description} + dirty={dirty} + disabled={disabled} + error={error} + id={id} + label={label} + name={name} + onBlur={onBlur} + onChange={onChange} + touched={touched} + type={type} + value={value as string} + /> + ) : ( + <ModalValidationField + description={description} + dirty={false} + error={undefined} + label={label} + touched={true} + > + {() => ( + <div className="sw-mb-5 sw-leading-6 sw-flex sw-items-center"> + <span className="sw-mr-1/2">{translate('webhooks.secret.field_mask.description')}</span> + <ButtonLink onClick={showSecretInput}> + {translate('webhooks.secret.field_mask.link')} + </ButtonLink> + </div> + )} + </ModalValidationField> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx index 4a97aa0b15c..75a89cefd4f 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx @@ -23,15 +23,15 @@ import ActionsDropdown, { ActionsDropdownItem, } from '../../../components/controls/ActionsDropdown'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook'; import CreateWebhookForm from './CreateWebhookForm'; import DeleteWebhookForm from './DeleteWebhookForm'; import DeliveriesForm from './DeliveriesForm'; interface Props { onDelete: (webhook: string) => Promise<void>; - onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; - webhook: Webhook; + onUpdate: (data: WebhookUpdatePayload) => Promise<void>; + webhook: WebhookResponse; } interface State { @@ -74,7 +74,7 @@ export default class WebhookActions extends React.PureComponent<Props, State> { this.setState({ deliveries: false }); }; - handleUpdate = (data: { name: string; url: string }) => { + handleUpdate = (data: { name: string; secret?: string; url: string }) => { return this.props.onUpdate({ ...data, webhook: this.props.webhook.key }); }; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx index 51ef3dc2236..1e69c832adc 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItem.tsx @@ -19,14 +19,14 @@ */ import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook'; import WebhookActions from './WebhookActions'; import WebhookItemLatestDelivery from './WebhookItemLatestDelivery'; interface Props { onDelete: (webhook: string) => Promise<void>; - onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; - webhook: Webhook; + onUpdate: (data: WebhookUpdatePayload) => Promise<void>; + webhook: WebhookResponse; } export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { @@ -34,11 +34,11 @@ export default function WebhookItem({ onDelete, onUpdate, webhook }: Props) { <tr> <td>{webhook.name}</td> <td>{webhook.url}</td> - <td>{webhook.secret ? translate('yes') : translate('no')}</td> + <td>{webhook.hasSecret ? translate('yes') : translate('no')}</td> <td> <WebhookItemLatestDelivery webhook={webhook} /> </td> - <td className="thin nowrap text-right"> + <td className="sw-text-right"> <WebhookActions onDelete={onDelete} onUpdate={onUpdate} webhook={webhook} /> </td> </tr> diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx index d932cf8633a..c896aa43738 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhookItemLatestDelivery.tsx @@ -24,11 +24,11 @@ import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import BulletListIcon from '../../../components/icons/BulletListIcon'; import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse } from '../../../types/webhook'; import LatestDeliveryForm from './LatestDeliveryForm'; interface Props { - webhook: Webhook; + webhook: WebhookResponse; } interface State { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx index 08135a70143..b60aa9008c4 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx @@ -20,13 +20,13 @@ import { sortBy } from 'lodash'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { Webhook } from '../../../types/webhook'; +import { WebhookResponse, WebhookUpdatePayload } from '../../../types/webhook'; import WebhookItem from './WebhookItem'; interface Props { onDelete: (webhook: string) => Promise<void>; - onUpdate: (data: { webhook: string; name: string; url: string }) => Promise<void>; - webhooks: Webhook[]; + onUpdate: (data: WebhookUpdatePayload) => Promise<void>; + webhooks: WebhookResponse[]; } export default class WebhooksList extends React.PureComponent<Props> { @@ -37,7 +37,7 @@ export default class WebhooksList extends React.PureComponent<Props> { <th>{translate('webhooks.url')}</th> <th>{translate('webhooks.secret_header')}</th> <th>{translate('webhooks.last_execution')}</th> - <th /> + <th className="sw-text-right">{translate('actions')}</th> </tr> </thead> ); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx index 69fd13fd6d4..1aa92f4b207 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx @@ -30,14 +30,14 @@ import { App } from '../App'; jest.mock('../../../../api/webhooks', () => ({ createWebhook: jest.fn(() => - Promise.resolve({ webhook: { key: '3', name: 'baz', url: 'http://baz' } }) + Promise.resolve({ webhook: { key: '3', name: 'baz', url: 'http://baz', hasSecret: false } }) ), deleteWebhook: jest.fn(() => Promise.resolve()), searchWebhooks: jest.fn(() => Promise.resolve({ webhooks: [ - { key: '1', name: 'foo', url: 'http://foo' }, - { key: '2', name: 'bar', url: 'http://bar' }, + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: false }, ], }) ), @@ -98,9 +98,9 @@ it('should correctly handle webhook creation', async () => { await new Promise(setImmediate); wrapper.update(); expect(wrapper.state('webhooks')).toEqual([ - { key: '1', name: 'foo', url: 'http://foo' }, - { key: '2', name: 'bar', url: 'http://bar' }, - { key: '3', name: 'baz', url: 'http://baz' }, + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: false }, + { key: '3', name: 'baz', url: 'http://baz', hasSecret: false }, ]); }); @@ -111,11 +111,13 @@ it('should correctly handle webhook deletion', async () => { await new Promise(setImmediate); wrapper.update(); - expect(wrapper.state('webhooks')).toEqual([{ key: '1', name: 'foo', url: 'http://foo' }]); + expect(wrapper.state('webhooks')).toEqual([ + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + ]); }); it('should correctly handle webhook update', async () => { - const newValues = { webhook: '1', name: 'Cfoo', url: 'http://cfoo' }; + const newValues = { webhook: '1', name: 'Cfoo', url: 'http://cfoo', secret: undefined }; const wrapper = shallow(<App />); (wrapper.instance() as App).handleUpdate(newValues); expect(updateWebhook).toHaveBeenLastCalledWith(newValues); @@ -123,7 +125,52 @@ it('should correctly handle webhook update', async () => { await new Promise(setImmediate); wrapper.update(); expect(wrapper.state('webhooks')).toEqual([ - { key: '1', name: 'Cfoo', url: 'http://cfoo' }, - { key: '2', name: 'bar', url: 'http://bar' }, + { key: '1', name: 'Cfoo', url: 'http://cfoo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: false }, + ]); +}); + +it('should correctly handle webhook secret update', async () => { + const newValuesWithSecret = { webhook: '2', name: 'bar', url: 'http://bar', secret: 'secret' }; + const newValuesWithoutSecret = { + webhook: '2', + name: 'bar', + url: 'http://bar', + secret: undefined, + }; + const newValuesWithEmptySecret = { webhook: '2', name: 'bar', url: 'http://bar', secret: '' }; + const wrapper = shallow(<App />); + + // With secret + (wrapper.instance() as App).handleUpdate(newValuesWithSecret); + expect(updateWebhook).toHaveBeenLastCalledWith(newValuesWithSecret); + + await new Promise(setImmediate); + wrapper.update(); + expect(wrapper.state('webhooks')).toEqual([ + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: true }, + ]); + + // Without secret + (wrapper.instance() as App).handleUpdate(newValuesWithoutSecret); + expect(updateWebhook).toHaveBeenLastCalledWith(newValuesWithoutSecret); + + await new Promise(setImmediate); + wrapper.update(); + expect(wrapper.state('webhooks')).toEqual([ + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: true }, + ]); + + // With empty secret + (wrapper.instance() as App).handleUpdate(newValuesWithEmptySecret); + expect(updateWebhook).toHaveBeenLastCalledWith(newValuesWithEmptySecret); + + await new Promise(setImmediate); + wrapper.update(); + expect(wrapper.state('webhooks')).toEqual([ + { key: '1', name: 'foo', url: 'http://foo', hasSecret: false }, + { key: '2', name: 'bar', url: 'http://bar', hasSecret: false }, ]); }); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-it.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-it.tsx new file mode 100644 index 00000000000..c56875897d3 --- /dev/null +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-it.tsx @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import userEvent from '@testing-library/user-event'; +import * as React from 'react'; +import { byLabelText, byRole } from 'testing-library-selector'; +import { renderComponent } from '../../../../helpers/testReactTestingUtils'; +import CreateWebhookForm from '../CreateWebhookForm'; + +const ui = { + nameInput: byRole('textbox', { name: 'webhooks.name field_required' }), + urlInput: byRole('textbox', { name: 'webhooks.url field_required' }), + secretInput: byLabelText('webhooks.secret'), + secretInputMaskButton: byRole('button', { name: 'webhooks.secret.field_mask.link' }), + createButton: byRole('button', { name: 'create' }), + updateButton: byRole('button', { name: 'update_verb' }), +}; + +describe('Webhook form', () => { + it('should correctly submit creation form', async () => { + const user = userEvent.setup(); + const webhook = { + name: 'foo', + url: 'http://bar', + secret: '', + }; + const onDone = jest.fn(); + + renderCreateWebhookForm({ onDone }); + + expect(ui.nameInput.get()).toHaveValue(''); + expect(ui.urlInput.get()).toHaveValue(''); + expect(ui.secretInput.get()).toHaveValue(''); + expect(ui.createButton.get()).toBeDisabled(); + + await user.type(ui.nameInput.get(), webhook.name); + await user.type(ui.urlInput.get(), webhook.url); + expect(ui.createButton.get()).toBeEnabled(); + + await user.click(ui.createButton.get()); + expect(onDone).toHaveBeenCalledWith(webhook); + }); + + it('should correctly submit update form', async () => { + const user = userEvent.setup(); + const webhook = { + hasSecret: false, + key: 'test-webhook-key', + name: 'foo', + url: 'http://bar', + }; + const nameExtension = 'bar'; + const url = 'http://bar'; + const onDone = jest.fn(); + + renderCreateWebhookForm({ onDone, webhook }); + + expect(ui.nameInput.get()).toHaveValue(webhook.name); + expect(ui.urlInput.get()).toHaveValue(webhook.url); + expect(ui.secretInput.query()).not.toBeInTheDocument(); + expect(ui.secretInputMaskButton.get()).toBeInTheDocument(); + expect(ui.updateButton.get()).toBeDisabled(); + + await user.type(ui.nameInput.get(), nameExtension); + await user.clear(ui.urlInput.get()); + await user.type(ui.urlInput.get(), url); + expect(ui.updateButton.get()).toBeEnabled(); + + await user.click(ui.updateButton.get()); + expect(onDone).toHaveBeenCalledWith({ + name: `${webhook.name}${nameExtension}`, + url, + secret: undefined, + }); + }); + + it('should correctly submit update form with empty secret', async () => { + const user = userEvent.setup(); + const webhook = { + hasSecret: false, + key: 'test-webhook-key', + name: 'foo', + url: 'http://bar', + }; + const onDone = jest.fn(); + + renderCreateWebhookForm({ onDone, webhook }); + + await user.click(ui.secretInputMaskButton.get()); + expect(ui.updateButton.get()).toBeEnabled(); + + await user.click(ui.updateButton.get()); + expect(onDone).toHaveBeenCalledWith({ + name: webhook.name, + url: webhook.url, + secret: '', + }); + }); + + it('should correctly submit update form with updated secret', async () => { + const user = userEvent.setup(); + const webhook = { + hasSecret: false, + key: 'test-webhook-key', + name: 'foo', + url: 'http://bar', + }; + const secret = 'test-webhook-secret'; + const onDone = jest.fn(); + + renderCreateWebhookForm({ onDone, webhook }); + + await user.click(ui.secretInputMaskButton.get()); + await user.type(ui.secretInput.get(), secret); + + await user.click(ui.updateButton.get()); + expect(onDone).toHaveBeenCalledWith({ + name: webhook.name, + url: webhook.url, + secret, + }); + }); +}); + +function renderCreateWebhookForm(props = {}) { + return renderComponent( + <CreateWebhookForm onClose={jest.fn()} onDone={jest.fn(() => Promise.resolve())} {...props} /> + ); +} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx deleted file mode 100644 index c8ec8b5ff71..00000000000 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/CreateWebhookForm-test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { shallow } from 'enzyme'; -import * as React from 'react'; -import CreateWebhookForm from '../CreateWebhookForm'; - -const webhookWithoutSecret = { key: '1', name: 'foo', url: 'http://foo.bar' }; -const webhookWithSecret = { key: '2', name: 'bar', secret: 'sonar', url: 'http://foo.bar' }; - -it('should render correctly when creating a new webhook', () => { - expect(getWrapper()).toMatchSnapshot(); -}); - -it('should render correctly when updating a webhook without secret', () => { - expect(getWrapper({ webhook: webhookWithoutSecret })).toMatchSnapshot(); -}); - -it('should render correctly when updating a webhook with a secret', () => { - expect(getWrapper({ webhook: webhookWithSecret })).toMatchSnapshot(); -}); - -function getWrapper(props = {}) { - return shallow( - <CreateWebhookForm onClose={jest.fn()} onDone={jest.fn(() => Promise.resolve())} {...props} /> - ); -} diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx index 7a04114d953..35f1d6da9aa 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx @@ -50,7 +50,7 @@ jest.mock('../../../../api/webhooks', () => ({ ), })); -const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; +const webhook = { key: '1', name: 'foo', url: 'http://foo.bar', hasSecret: false }; beforeEach(() => { (searchDeliveries as jest.Mock<any>).mockClear(); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx index 0aac1590d78..83756ec0ee7 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/LatestDeliveryForm-test.tsx @@ -38,7 +38,7 @@ const delivery = { success: true, }; -const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' }; +const webhook = { key: '1', name: 'foo', url: 'http://foo.bar', hasSecret: false }; beforeEach(() => { (getDelivery as jest.Mock<any>).mockClear(); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx index 6c5bb8f0616..f9e3da4f89f 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx @@ -26,6 +26,7 @@ const webhook = { key: '1', name: 'foo', url: 'http://foo.bar', + hasSecret: false, }; const delivery = { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx index 1e5361f5b46..7741671cd32 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItem-test.tsx @@ -21,10 +21,18 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import WebhookItem from '../WebhookItem'; -const webhook = { +const webhookWithoutSecret = { key: '1', name: 'my webhook', url: 'http://webhook.target', + hasSecret: false, +}; + +const webhookWithSecret = { + key: '1', + name: 'my webhook', + url: 'http://webhook.target', + hasSecret: true, }; it('should render correctly', () => { @@ -33,7 +41,16 @@ it('should render correctly', () => { <WebhookItem onDelete={jest.fn(() => Promise.resolve())} onUpdate={jest.fn(() => Promise.resolve())} - webhook={webhook} + webhook={webhookWithoutSecret} + /> + ) + ).toMatchSnapshot(); + expect( + shallow( + <WebhookItem + onDelete={jest.fn(() => Promise.resolve())} + onUpdate={jest.fn(() => Promise.resolve())} + webhook={webhookWithSecret} /> ) ).toMatchSnapshot(); diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx index 312bb572b7d..ea960a1e764 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookItemLatestDelivery-test.tsx @@ -34,6 +34,7 @@ const webhook = { key: '1', name: 'my webhook', url: 'http://webhook.target', + hasSecret: false, latestDelivery, }; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx index 15210f6c747..2f3bcace219 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhooksList-test.tsx @@ -22,8 +22,8 @@ import * as React from 'react'; import WebhooksList from '../WebhooksList'; const webhooks = [ - { key: '1', name: 'my webhook', url: 'http://webhook.target' }, - { key: '2', name: 'jenkins webhook', url: 'http://jenkins.target' }, + { key: '1', name: 'my webhook', url: 'http://webhook.target', hasSecret: false }, + { key: '2', name: 'jenkins webhook', url: 'http://jenkins.target', hasSecret: false }, ]; it('should correctly render empty webhook list', () => { diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap index 7a0ea7fcc9f..ba0b2bb2a08 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap @@ -59,11 +59,13 @@ exports[`should fetch webhooks and display them 1`] = ` webhooks={ [ { + "hasSecret": false, "key": "1", "name": "foo", "url": "http://foo", }, { + "hasSecret": false, "key": "2", "name": "bar", "url": "http://bar", diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap deleted file mode 100644 index e66235b6d61..00000000000 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/CreateWebhookForm-test.tsx.snap +++ /dev/null @@ -1,61 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`should render correctly when creating a new webhook 1`] = ` -<ValidationModal - confirmButtonText="create" - header="webhooks.create" - initialValues={ - { - "name": "", - "secret": "", - "url": "", - } - } - onClose={[MockFunction]} - onSubmit={[MockFunction]} - size="small" - validate={[Function]} -> - <Component /> -</ValidationModal> -`; - -exports[`should render correctly when updating a webhook with a secret 1`] = ` -<ValidationModal - confirmButtonText="update_verb" - header="webhooks.update" - initialValues={ - { - "name": "bar", - "secret": "sonar", - "url": "http://foo.bar", - } - } - onClose={[MockFunction]} - onSubmit={[MockFunction]} - size="small" - validate={[Function]} -> - <Component /> -</ValidationModal> -`; - -exports[`should render correctly when updating a webhook without secret 1`] = ` -<ValidationModal - confirmButtonText="update_verb" - header="webhooks.update" - initialValues={ - { - "name": "foo", - "secret": "", - "url": "http://foo.bar", - } - } - onClose={[MockFunction]} - onSubmit={[MockFunction]} - size="small" - validate={[Function]} -> - <Component /> -</ValidationModal> -`; diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap index cc748f5df22..6e31377430a 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookItem-test.tsx.snap @@ -15,6 +15,7 @@ exports[`should render correctly 1`] = ` <WebhookItemLatestDelivery webhook={ { + "hasSecret": false, "key": "1", "name": "my webhook", "url": "http://webhook.target", @@ -23,13 +24,56 @@ exports[`should render correctly 1`] = ` /> </td> <td - className="thin nowrap text-right" + className="sw-text-right" > <WebhookActions onDelete={[MockFunction]} onUpdate={[MockFunction]} webhook={ { + "hasSecret": false, + "key": "1", + "name": "my webhook", + "url": "http://webhook.target", + } + } + /> + </td> +</tr> +`; + +exports[`should render correctly 2`] = ` +<tr> + <td> + my webhook + </td> + <td> + http://webhook.target + </td> + <td> + yes + </td> + <td> + <WebhookItemLatestDelivery + webhook={ + { + "hasSecret": true, + "key": "1", + "name": "my webhook", + "url": "http://webhook.target", + } + } + /> + </td> + <td + className="sw-text-right" + > + <WebhookActions + onDelete={[MockFunction]} + onUpdate={[MockFunction]} + webhook={ + { + "hasSecret": true, "key": "1", "name": "my webhook", "url": "http://webhook.target", diff --git a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap index cf08f013601..07ad28daa69 100644 --- a/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap @@ -24,7 +24,11 @@ exports[`should correctly render the webhooks 1`] = ` <th> webhooks.last_execution </th> - <th /> + <th + className="sw-text-right" + > + actions + </th> </tr> </thead> <tbody> @@ -34,6 +38,7 @@ exports[`should correctly render the webhooks 1`] = ` onUpdate={[MockFunction]} webhook={ { + "hasSecret": false, "key": "2", "name": "jenkins webhook", "url": "http://jenkins.target", @@ -46,6 +51,7 @@ exports[`should correctly render the webhooks 1`] = ` onUpdate={[MockFunction]} webhook={ { + "hasSecret": false, "key": "1", "name": "my webhook", "url": "http://webhook.target", |