Browse Source

SONAR-10347 Add ability to browse webhook deliveries payloads

tags/7.5
Grégoire Aubert 6 years ago
parent
commit
4bf292bac1
24 changed files with 858 additions and 83 deletions
  1. 20
    1
      server/sonar-web/src/main/js/api/webhooks.ts
  2. 2
    1
      server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx
  3. 2
    1
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx
  4. 7
    7
      server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx
  5. 24
    19
      server/sonar-web/src/main/js/apps/webhooks/components/App.tsx
  6. 4
    3
      server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx
  7. 120
    0
      server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx
  8. 119
    0
      server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx
  9. 4
    2
      server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx
  10. 20
    3
      server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx
  11. 2
    1
      server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx
  12. 68
    5
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx
  13. 74
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx
  14. 58
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx
  15. 6
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx
  16. 47
    30
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap
  17. 107
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap
  18. 84
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap
  19. 2
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap
  20. 6
    0
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap
  21. 8
    8
      server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap
  22. 54
    1
      server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts
  23. 14
    0
      server/sonar-web/src/main/js/helpers/urls.ts
  24. 6
    1
      sonar-core/src/main/resources/org/sonar/l10n/core.properties

+ 20
- 1
server/sonar-web/src/main/js/api/webhooks.ts View File

@@ -19,7 +19,7 @@
*/
import { getJSON, post, postJSON } from '../helpers/request';
import throwGlobalError from '../app/utils/throwGlobalError';
import { Webhook } from '../app/types';
import { Webhook, WebhookDelivery, Paging } from '../app/types';

export function createWebhook(data: {
name: string;
@@ -48,3 +48,22 @@ export function updateWebhook(data: {
}): Promise<void | Response> {
return post('/api/webhooks/update', data).catch(throwGlobalError);
}

export function searchDeliveries(data: {
ceTaskId?: string;
componentKey?: string;
webhook?: string;
p?: number;
ps?: number;
}): Promise<{
deliveries: WebhookDelivery[];
paging: Paging;
}> {
return getJSON('/api/webhooks/deliveries', data).catch(throwGlobalError);
}

export function getDelivery(data: {
deliveryId: string;
}): Promise<{ delivery: WebhookDelivery & { payload: string } }> {
return getJSON('/api/webhooks/delivery', data).catch(throwGlobalError);
}

+ 2
- 1
server/sonar-web/src/main/js/app/components/nav/component/ComponentNavMenu.tsx View File

@@ -42,7 +42,8 @@ const SETTINGS_URLS = [
'/project/history',
'background_tasks',
'/project/key',
'/project/deletion'
'/project/deletion',
'/project/webhooks'
];

interface Props {

+ 2
- 1
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationAdministration.tsx View File

@@ -36,7 +36,8 @@ const ADMIN_PATHS = [
'delete',
'permissions',
'permission_templates',
'projects_management'
'projects_management',
'webhooks'
];

export default function OrganizationNavigationAdministration({ location, organization }: Props) {

+ 7
- 7
server/sonar-web/src/main/js/apps/organizations/navigation/OrganizationNavigationMenu.tsx View File

@@ -35,37 +35,37 @@ export default function OrganizationNavigationMenu({ location, organization }: P
return (
<NavBarTabs className="navbar-context-tabs">
<li>
<Link to={`/organizations/${organization.key}/projects`} activeClassName="active">
<Link activeClassName="active" to={`/organizations/${organization.key}/projects`}>
{translate('projects.page')}
</Link>
</li>
<li>
<Link
activeClassName="active"
to={{
pathname: `/organizations/${organization.key}/issues`,
query: { resolved: 'false' }
}}
activeClassName="active">
}}>
{translate('issues.page')}
</Link>
</li>
<li>
<Link to={`/organizations/${organization.key}/quality_profiles`} activeClassName="active">
<Link activeClassName="active" to={`/organizations/${organization.key}/quality_profiles`}>
{translate('quality_profiles.page')}
</Link>
</li>
<li>
<Link to={`/organizations/${organization.key}/rules`} activeClassName="active">
<Link activeClassName="active" to={`/organizations/${organization.key}/rules`}>
{translate('coding_rules.page')}
</Link>
</li>
<li>
<Link to={getQualityGatesUrl(organization.key)} activeClassName="active">
<Link activeClassName="active" to={getQualityGatesUrl(organization.key)}>
{translate('quality_gates.page')}
</Link>
</li>
<li>
<Link to={`/organizations/${organization.key}/members`} activeClassName="active">
<Link activeClassName="active" to={`/organizations/${organization.key}/members`}>
{translate('organization.members.page')}
</Link>
</li>

+ 24
- 19
server/sonar-web/src/main/js/apps/webhooks/components/App.tsx View File

@@ -94,7 +94,10 @@ export default class App extends React.PureComponent<Props, State> {
if (this.mounted) {
this.setState(({ webhooks }) => ({
webhooks: webhooks.map(
webhook => (webhook.key === data.webhook ? { ...webhook, ...data } : webhook)
webhook =>
webhook.key === data.webhook
? { ...webhook, name: data.name, url: data.url }
: webhook
)
}));
}
@@ -105,27 +108,29 @@ export default class App extends React.PureComponent<Props, State> {
const { loading, webhooks } = this.state;

return (
<div className="page page-limited">
<>
<Helmet title={translate('webhooks.page')} />

<PageHeader loading={loading}>
<PageActions
loading={loading}
onCreate={this.handleCreate}
webhooksCount={webhooks.length}
/>
</PageHeader>

{!loading && (
<div className="boxed-group boxed-group-inner">
<WebhooksList
onDelete={this.handleDelete}
onUpdate={this.handleUpdate}
webhooks={webhooks}
<div className="page page-limited">
<PageHeader loading={loading}>
<PageActions
loading={loading}
onCreate={this.handleCreate}
webhooksCount={webhooks.length}
/>
</div>
)}
</div>
</PageHeader>

{!loading && (
<div className="boxed-group boxed-group-inner">
<WebhooksList
onDelete={this.handleDelete}
onUpdate={this.handleUpdate}
webhooks={webhooks}
/>
</div>
)}
</div>
</>
);
}
}

+ 4
- 3
server/sonar-web/src/main/js/apps/webhooks/components/CreateWebhookForm.tsx View File

@@ -22,6 +22,7 @@ import { FormikProps } from 'formik';
import ValidationModal from '../../../components/controls/ValidationModal';
import InputValidationField from '../../../components/controls/InputValidationField';
import { Webhook } from '../../../app/types';
import { isUrl } from '../../../helpers/urls';
import { translate } from '../../../helpers/l10n';

interface Props {
@@ -52,8 +53,8 @@ export default class CreateWebhookForm extends React.PureComponent<Props> {
errors.url = translate('webhooks.url.required');
} else if (!url.startsWith('http://') && !url.startsWith('https://')) {
errors.url = translate('webhooks.url.bad_protocol');
} else if (url.indexOf(':', 6) > 0 && url.indexOf('@') <= 0) {
errors.url = translate('webhooks.url.bad_auth');
} else if (!isUrl(url)) {
errors.url = translate('webhooks.url.bad_format');
}
return errors;
};
@@ -106,8 +107,8 @@ export default class CreateWebhookForm extends React.PureComponent<Props> {
/>
<InputValidationField
description={translate('webhooks.url.description')}
disabled={isSubmitting}
dirty={dirty}
disabled={isSubmitting}
error={errors.url}
id="webhook-url"
label={

+ 120
- 0
server/sonar-web/src/main/js/apps/webhooks/components/DeliveriesForm.tsx View File

@@ -0,0 +1,120 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import DeliveryItem from './DeliveryItem';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import ListFooter from '../../../components/controls/ListFooter';
import Modal from '../../../components/controls/Modal';
import { Webhook, WebhookDelivery, Paging } from '../../../app/types';
import { translateWithParameters, translate } from '../../../helpers/l10n';
import { searchDeliveries } from '../../../api/webhooks';

interface Props {
onClose: () => void;
webhook: Webhook;
}

interface State {
deliveries: WebhookDelivery[];
loading: boolean;
paging?: Paging;
}

const PAGE_SIZE = 10;

export default class DeliveriesForm extends React.PureComponent<Props, State> {
mounted: boolean = false;
state: State = { deliveries: [], loading: true };

componentDidMount() {
this.mounted = true;
this.fetchDeliveries();
}

componentWillUnmount() {
this.mounted = false;
}

fetchDeliveries = ({ webhook } = this.props) => {
searchDeliveries({ webhook: webhook.key, ps: PAGE_SIZE }).then(({ deliveries, paging }) => {
if (this.mounted) {
this.setState({ deliveries, loading: false, paging });
}
}, this.stopLoading);
};

fetchMoreDeliveries = ({ webhook } = this.props) => {
const { paging } = this.state;
if (paging) {
this.setState({ loading: true });
searchDeliveries({ webhook: webhook.key, p: paging.pageIndex + 1, ps: PAGE_SIZE }).then(
({ deliveries, paging }) => {
if (this.mounted) {
this.setState((state: State) => ({
deliveries: [...state.deliveries, ...deliveries],
loading: false,
paging
}));
}
},
this.stopLoading
);
}
};

stopLoading = () => {
if (this.mounted) {
this.setState({ loading: false });
}
};

render() {
const { webhook } = this.props;
const { deliveries, loading, paging } = this.state;
const header = translateWithParameters('webhooks.deliveries_for_x', webhook.name);

return (
<Modal contentLabel={header} onRequestClose={this.props.onClose}>
<header className="modal-head">
<h2>{header}</h2>
</header>
<div className="modal-body modal-container">
{deliveries.map(delivery => <DeliveryItem delivery={delivery} key={delivery.id} />)}
<div className="text-center">
<DeferredSpinner loading={loading} />
</div>
{paging !== undefined && (
<ListFooter
count={deliveries.length}
loadMore={this.fetchMoreDeliveries}
ready={!loading}
total={paging.total}
/>
)}
</div>
<footer className="modal-foot">
<button className="button-link js-modal-close" onClick={this.props.onClose} type="button">
{translate('close')}
</button>
</footer>
</Modal>
);
}
}

+ 119
- 0
server/sonar-web/src/main/js/apps/webhooks/components/DeliveryItem.tsx View File

@@ -0,0 +1,119 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import CheckIcon from '../../../components/icons-components/CheckIcon';
import ClearIcon from '../../../components/icons-components/ClearIcon';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import BoxedGroupAccordion from '../../../components/controls/BoxedGroupAccordion';
import CodeSnippet from '../../../components/common/CodeSnippet';
import DeferredSpinner from '../../../components/common/DeferredSpinner';
import { getDelivery } from '../../../api/webhooks';
import { formatMeasure } from '../../../helpers/measures';
import { translateWithParameters, translate } from '../../../helpers/l10n';
import { WebhookDelivery } from '../../../app/types';

interface Props {
delivery: WebhookDelivery;
}

interface State {
loading: boolean;
open: boolean;
payload?: string;
}

export default class DeliveryItem extends React.PureComponent<Props, State> {
mounted: boolean = false;
state: State = { loading: false, open: false };

componentDidMount() {
this.mounted = true;
}

componentWillUnmount() {
this.mounted = false;
}

fetchPayload = ({ delivery } = this.props) => {
this.setState({ loading: true });
return getDelivery({ deliveryId: delivery.id }).then(
({ delivery }) => {
if (this.mounted) {
this.setState({ payload: delivery.payload, loading: false });
}
},
() => {
if (this.mounted) {
this.setState({ loading: false });
}
}
);
};

formatPayload = (payload: string) => {
try {
return JSON.stringify(JSON.parse(payload), undefined, 2);
} catch (error) {
return payload;
}
};

handleClick = () => {
if (!this.state.payload) {
this.fetchPayload();
}
this.setState(({ open }) => ({ open: !open }));
};

render() {
const { delivery } = this.props;
const { loading, open, payload } = this.state;

return (
<BoxedGroupAccordion
onClick={this.handleClick}
open={open}
renderHeader={() =>
delivery.success ? (
<AlertSuccessIcon className="pull-right js-success" />
) : (
<AlertErrorIcon className="pull-right js-error" />
)
}
title={<DateTimeFormatter date={delivery.at} />}>
<div className="big-spacer-left">
<p className="spacer-bottom">
{translateWithParameters('webhooks.delivery.response_x', delivery.httpStatus)}
</p>
<p className="spacer-bottom">
{translateWithParameters(
'webhooks.delivery.duration_x',
formatMeasure(delivery.durationMs, 'MILLISEC')
)}
</p>
<p className="spacer-bottom">{translate('webhooks.delivery.payload')}</p>
<DeferredSpinner className="spacer-left spacer-top" loading={loading}>
{payload && <CodeSnippet noCopy={true} snippet={this.formatPayload(payload)} />}
</DeferredSpinner>
</div>
</BoxedGroupAccordion>
);
}
}

+ 4
- 2
server/sonar-web/src/main/js/apps/webhooks/components/PageActions.tsx View File

@@ -62,14 +62,16 @@ export default class PageActions extends React.PureComponent<Props, State> {
<Tooltip
overlay={translateWithParameters('webhooks.maximum_reached', WEBHOOKS_LIMIT)}
placement="left">
<button className="js-webhook-create disabled">{translate('create')}</button>
<button className="js-webhook-create disabled" type="button">
{translate('create')}
</button>
</Tooltip>
);
}

return (
<>
<button className="js-webhook-create" onClick={this.handleCreateOpen}>
<button className="js-webhook-create" onClick={this.handleCreateOpen} type="button">
{translate('create')}
</button>
{this.state.openCreate && (

+ 20
- 3
server/sonar-web/src/main/js/apps/webhooks/components/WebhookActions.tsx View File

@@ -19,6 +19,7 @@
*/
import * as React from 'react';
import CreateWebhookForm from './CreateWebhookForm';
import DeliveriesForm from './DeliveriesForm';
import ActionsDropdown, {
ActionsDropdownItem,
ActionsDropdownDivider
@@ -34,12 +35,13 @@ interface Props {
}

interface State {
deliveries: boolean;
updating: boolean;
}

export default class WebhookActions extends React.PureComponent<Props, State> {
mounted: boolean = false;
state: State = { updating: false };
state: State = { deliveries: false, updating: false };

componentDidMount() {
this.mounted = true;
@@ -53,6 +55,14 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
return this.props.onDelete(this.props.webhook.key);
};

handleDeliveriesClick = () => {
this.setState({ deliveries: true });
};

handleDeliveriesStop = () => {
this.setState({ deliveries: false });
};

handleUpdate = (data: { name: string; url: string }) => {
return this.props.onUpdate({ ...data, webhook: this.props.webhook.key });
};
@@ -67,13 +77,18 @@ export default class WebhookActions extends React.PureComponent<Props, State> {

render() {
const { webhook } = this.props;
// TODO Disable "Show deliveries" if there is no lastDelivery
return (
<>
<ActionsDropdown className="big-spacer-left">
<ActionsDropdownItem className="js-webhook-update" onClick={this.handleUpdateClick}>
{translate('update_verb')}
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-webhook-deliveries"
onClick={this.handleDeliveriesClick}>
{translate('webhooks.deliveries.show')}
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ConfirmButton
confirmButtonText={translate('delete')}
@@ -91,7 +106,9 @@ export default class WebhookActions extends React.PureComponent<Props, State> {
)}
</ConfirmButton>
</ActionsDropdown>

{this.state.deliveries && (
<DeliveriesForm onClose={this.handleDeliveriesStop} webhook={webhook} />
)}
{this.state.updating && (
<CreateWebhookForm
onClose={this.handleUpdatingStop}

+ 2
- 1
server/sonar-web/src/main/js/apps/webhooks/components/WebhooksList.tsx View File

@@ -18,6 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { sortBy } from 'lodash';
import WebhookItem from './WebhookItem';
import { Webhook } from '../../../app/types';
import { translate } from '../../../helpers/l10n';
@@ -48,7 +49,7 @@ export default class WebhooksList extends React.PureComponent<Props> {
<table className="data zebra">
{this.renderHeader()}
<tbody>
{webhooks.map(webhook => (
{sortBy(webhooks, webhook => webhook.name.toLowerCase()).map(webhook => (
<WebhookItem
key={webhook.key}
onDelete={this.props.onDelete}

+ 68
- 5
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/App-test.tsx View File

@@ -20,18 +20,38 @@
import * as React from 'react';
import { shallow } from 'enzyme';
import App from '../App';
import { searchWebhooks } from '../../../../api/webhooks';
import {
createWebhook,
deleteWebhook,
searchWebhooks,
updateWebhook
} from '../../../../api/webhooks';
import { Visibility } from '../../../../app/types';

jest.mock('../../../../api/webhooks', () => ({
searchWebhooks: jest.fn(() => Promise.resolve({ webhooks: [] }))
createWebhook: jest.fn(() =>
Promise.resolve({ webhook: { key: '3', name: 'baz', url: 'http://baz' } })
),
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' }
]
})
),
updateWebhook: jest.fn(() => Promise.resolve())
}));

const organization = { key: 'foo', name: 'Foo', projectVisibility: Visibility.Private };
const component = { key: 'bar', organization: 'foo', qualifier: 'TRK' };

beforeEach(() => {
(createWebhook as jest.Mock<any>).mockClear();
(deleteWebhook as jest.Mock<any>).mockClear();
(searchWebhooks as jest.Mock<any>).mockClear();
(updateWebhook as jest.Mock<any>).mockClear();
});

it('should be in loading status', () => {
@@ -58,21 +78,21 @@ describe('should correctly fetch webhooks when', () => {
});

it('on project scope', async () => {
shallow(<App organization={undefined} component={component} />);
shallow(<App component={component} organization={undefined} />);

await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({ project: component.key });
});

it('on organization scope', async () => {
shallow(<App organization={organization} component={undefined} />);
shallow(<App component={undefined} organization={organization} />);

await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({ organization: organization.key });
});

it('on project scope within an organization', async () => {
shallow(<App organization={organization} component={component} />);
shallow(<App component={component} organization={organization} />);

await new Promise(setImmediate);
expect(searchWebhooks).toHaveBeenCalledWith({
@@ -81,3 +101,46 @@ describe('should correctly fetch webhooks when', () => {
});
});
});

it('should correctly handle webhook creation', async () => {
const webhook = { name: 'baz', url: 'http://baz' };
const wrapper = shallow(<App organization={organization} />);
(wrapper.instance() as App).handleCreate({ ...webhook });
expect(createWebhook).lastCalledWith({
...webhook,
organization: organization.key,
project: undefined
});

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' }
]);
});

it('should correctly handle webhook deletion', async () => {
const wrapper = shallow(<App organization={undefined} />);
(wrapper.instance() as App).handleDelete('2');
expect(deleteWebhook).lastCalledWith({ webhook: '2' });

await new Promise(setImmediate);
wrapper.update();
expect(wrapper.state('webhooks')).toEqual([{ key: '1', name: 'foo', url: 'http://foo' }]);
});

it('should correctly handle webhook update', async () => {
const newValues = { webhook: '1', name: 'Cfoo', url: 'http://cfoo' };
const wrapper = shallow(<App organization={undefined} />);
(wrapper.instance() as App).handleUpdate(newValues);
expect(updateWebhook).lastCalledWith(newValues);

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' }
]);
});

+ 74
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveriesForm-test.tsx View File

@@ -0,0 +1,74 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { shallow } from 'enzyme';
import DeliveriesForm from '../DeliveriesForm';
import { searchDeliveries } from '../../../../api/webhooks';

jest.mock('../../../../api/webhooks', () => ({
searchDeliveries: jest.fn(() =>
Promise.resolve({
deliveries: [
{
at: '12.02.2018',
durationMs: 20,
httpStatus: 200,
id: '2',
success: true
},
{
at: '11.02.2018',
durationMs: 122,
httpStatus: 500,
id: '1',
success: false
}
],
paging: {
pageIndex: 1,
pageSize: 10,
total: 15
}
})
)
}));

const webhook = { key: '1', name: 'foo', url: 'http://foo.bar' };

beforeEach(() => {
(searchDeliveries as jest.Mock<any>).mockClear();
});

it('should render correctly', async () => {
const wrapper = getWrapper();
expect(wrapper).toMatchSnapshot();

await new Promise(setImmediate);
expect(searchDeliveries as jest.Mock<any>).lastCalledWith({ webhook: webhook.key, ps: 10 });
wrapper.update();
expect(wrapper).toMatchSnapshot();

wrapper.find('ListFooter').prop<Function>('loadMore')();
expect(searchDeliveries).lastCalledWith({ webhook: webhook.key, p: 2, ps: 10 });
});

function getWrapper(props = {}) {
return shallow(<DeliveriesForm onClose={jest.fn()} webhook={webhook} {...props} />);
}

+ 58
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/DeliveryItem-test.tsx View File

@@ -0,0 +1,58 @@
/*
* SonarQube
* Copyright (C) 2009-2018 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 * as React from 'react';
import { shallow } from 'enzyme';
import DeliveryItem from '../DeliveryItem';
import { getDelivery } from '../../../../api/webhooks';

jest.mock('../../../../api/webhooks', () => ({
getDelivery: jest.fn(() =>
Promise.resolve({
delivery: { payload: '{ "success": true }' }
})
)
}));

const delivery = {
at: '12.02.2018',
durationMs: 20,
httpStatus: 200,
id: '2',
success: true
};

beforeEach(() => {
(getDelivery as jest.Mock<any>).mockClear();
});

it('should render correctly', async () => {
const wrapper = getWrapper();
expect(wrapper).toMatchSnapshot();

wrapper.find('BoxedGroupAccordion').prop<Function>('onClick')();
await new Promise(setImmediate);
expect(getDelivery).lastCalledWith({ deliveryId: delivery.id });
wrapper.update();
expect(wrapper).toMatchSnapshot();
});

function getWrapper(props = {}) {
return shallow(<DeliveryItem delivery={delivery} {...props} />);
}

+ 6
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/WebhookActions-test.tsx View File

@@ -54,6 +54,12 @@ it('should display the delete webhook form', () => {
expect(onDelete).lastCalledWith(webhook.key);
});

it('should display the deliveries form', () => {
const wrapper = getWrapper();
click(wrapper.find('.js-webhook-deliveries'));
expect(wrapper.find('DeliveriesForm').exists()).toBeTruthy();
});

function getWrapper(props = {}) {
return shallow(
<WebhookActions

+ 47
- 30
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/App-test.tsx.snap View File

@@ -1,52 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should be in loading status 1`] = `
<div
className="page page-limited"
>
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="webhooks.page"
/>
<PageHeader
loading={true}
<div
className="page page-limited"
>
<PageActions
<PageHeader
loading={true}
onCreate={[Function]}
webhooksCount={0}
/>
</PageHeader>
</div>
>
<PageActions
loading={true}
onCreate={[Function]}
webhooksCount={0}
/>
</PageHeader>
</div>
</React.Fragment>
`;

exports[`should fetch webhooks and display them 1`] = `
<div
className="page page-limited"
>
<React.Fragment>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
title="webhooks.page"
/>
<PageHeader
loading={false}
>
<PageActions
loading={false}
onCreate={[Function]}
webhooksCount={0}
/>
</PageHeader>
<div
className="boxed-group boxed-group-inner"
className="page page-limited"
>
<WebhooksList
onDelete={[Function]}
onUpdate={[Function]}
webhooks={Array []}
/>
<PageHeader
loading={false}
>
<PageActions
loading={false}
onCreate={[Function]}
webhooksCount={2}
/>
</PageHeader>
<div
className="boxed-group boxed-group-inner"
>
<WebhooksList
onDelete={[Function]}
onUpdate={[Function]}
webhooks={
Array [
Object {
"key": "1",
"name": "foo",
"url": "http://foo",
},
Object {
"key": "2",
"name": "bar",
"url": "http://bar",
},
]
}
/>
</div>
</div>
</div>
</React.Fragment>
`;

+ 107
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveriesForm-test.tsx.snap View File

@@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<Modal
contentLabel="webhooks.deliveries_for_x.foo"
onRequestClose={[MockFunction]}
>
<header
className="modal-head"
>
<h2>
webhooks.deliveries_for_x.foo
</h2>
</header>
<div
className="modal-body modal-container"
>
<div
className="text-center"
>
<DeferredSpinner
loading={true}
timeout={100}
/>
</div>
</div>
<footer
className="modal-foot"
>
<button
className="button-link js-modal-close"
onClick={[MockFunction]}
type="button"
>
close
</button>
</footer>
</Modal>
`;

exports[`should render correctly 2`] = `
<Modal
contentLabel="webhooks.deliveries_for_x.foo"
onRequestClose={[MockFunction]}
>
<header
className="modal-head"
>
<h2>
webhooks.deliveries_for_x.foo
</h2>
</header>
<div
className="modal-body modal-container"
>
<DeliveryItem
delivery={
Object {
"at": "12.02.2018",
"durationMs": 20,
"httpStatus": 200,
"id": "2",
"success": true,
}
}
key="2"
/>
<DeliveryItem
delivery={
Object {
"at": "11.02.2018",
"durationMs": 122,
"httpStatus": 500,
"id": "1",
"success": false,
}
}
key="1"
/>
<div
className="text-center"
>
<DeferredSpinner
loading={false}
timeout={100}
/>
</div>
<ListFooter
count={2}
loadMore={[Function]}
ready={true}
total={15}
/>
</div>
<footer
className="modal-foot"
>
<button
className="button-link js-modal-close"
onClick={[MockFunction]}
type="button"
>
close
</button>
</footer>
</Modal>
`;

+ 84
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/DeliveryItem-test.tsx.snap View File

@@ -0,0 +1,84 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<BoxedGroupAccordion
onClick={[Function]}
open={false}
renderHeader={[Function]}
title={
<DateTimeFormatter
date="12.02.2018"
/>
}
>
<div
className="big-spacer-left"
>
<p
className="spacer-bottom"
>
webhooks.delivery.response_x.200
</p>
<p
className="spacer-bottom"
>
webhooks.delivery.duration_x.20ms
</p>
<p
className="spacer-bottom"
>
webhooks.delivery.payload
</p>
<DeferredSpinner
className="spacer-left spacer-top"
loading={false}
timeout={100}
/>
</div>
</BoxedGroupAccordion>
`;

exports[`should render correctly 2`] = `
<BoxedGroupAccordion
onClick={[Function]}
open={true}
renderHeader={[Function]}
title={
<DateTimeFormatter
date="12.02.2018"
/>
}
>
<div
className="big-spacer-left"
>
<p
className="spacer-bottom"
>
webhooks.delivery.response_x.200
</p>
<p
className="spacer-bottom"
>
webhooks.delivery.duration_x.20ms
</p>
<p
className="spacer-bottom"
>
webhooks.delivery.payload
</p>
<DeferredSpinner
className="spacer-left spacer-top"
loading={false}
timeout={100}
>
<CodeSnippet
noCopy={true}
snippet="{
\\"success\\": true
}"
/>
</DeferredSpinner>
</div>
</BoxedGroupAccordion>
`;

+ 2
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/PageActions-test.tsx.snap View File

@@ -10,6 +10,7 @@ exports[`should not allow to create a new webhook 1`] = `
>
<button
className="js-webhook-create disabled"
type="button"
>
create
</button>
@@ -25,6 +26,7 @@ exports[`should render correctly 1`] = `
<button
className="js-webhook-create"
onClick={[Function]}
type="button"
>
create
</button>

+ 6
- 0
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhookActions-test.tsx.snap View File

@@ -11,6 +11,12 @@ exports[`should render correctly 1`] = `
>
update_verb
</ActionsDropdownItem>
<ActionsDropdownItem
className="js-webhook-deliveries"
onClick={[Function]}
>
webhooks.deliveries.show
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ConfirmButton
confirmButtonText="delete"

+ 8
- 8
server/sonar-web/src/main/js/apps/webhooks/components/__tests__/__snapshots__/WebhooksList-test.tsx.snap View File

@@ -23,26 +23,26 @@ exports[`should correctly render the webhooks 1`] = `
</thead>
<tbody>
<WebhookItem
key="1"
key="2"
onDelete={[MockFunction]}
onUpdate={[MockFunction]}
webhook={
Object {
"key": "1",
"name": "my webhook",
"url": "http://webhook.target",
"key": "2",
"name": "jenkins webhook",
"url": "http://jenkins.target",
}
}
/>
<WebhookItem
key="2"
key="1"
onDelete={[MockFunction]}
onUpdate={[MockFunction]}
webhook={
Object {
"key": "2",
"name": "jenkins webhook",
"url": "http://jenkins.target",
"key": "1",
"name": "my webhook",
"url": "http://webhook.target",
}
}
/>

+ 54
- 1
server/sonar-web/src/main/js/helpers/__tests__/urls-test.ts View File

@@ -23,7 +23,8 @@ import {
getPathUrlAsString,
getProjectUrl,
getQualityGatesUrl,
getQualityGateUrl
getQualityGateUrl,
isUrl
} from '../urls';

const SIMPLE_COMPONENT_KEY = 'sonarqube';
@@ -113,3 +114,55 @@ describe('#getQualityGate(s)Url', () => {
});
});
});

describe('#isUrl', () => {
it('should be valid', () => {
expect(isUrl('https://localhost')).toBeTruthy();
expect(isUrl('https://localhost/')).toBeTruthy();
expect(isUrl('https://localhost:9000')).toBeTruthy();
expect(isUrl('https://localhost:9000/')).toBeTruthy();
expect(isUrl('https://foo:bar@localhost:9000')).toBeTruthy();
expect(isUrl('https://foo@localhost')).toBeTruthy();
expect(isUrl('http://foo.com/blah_blah')).toBeTruthy();
expect(isUrl('http://foo.com/blah_blah/')).toBeTruthy();
expect(isUrl('http://www.example.com/wpstyle/?p=364')).toBeTruthy();
expect(isUrl('https://www.example.com/foo/?bar=baz&inga=42&quux')).toBeTruthy();
expect(isUrl('http://userid@example.com')).toBeTruthy();
expect(isUrl('http://userid@example.com/')).toBeTruthy();
expect(isUrl('http://userid:password@example.com:8080')).toBeTruthy();
expect(isUrl('http://userid:password@example.com:8080/')).toBeTruthy();
expect(isUrl('http://userid@example.com:8080')).toBeTruthy();
expect(isUrl('http://userid@example.com:8080/')).toBeTruthy();
expect(isUrl('http://userid:password@example.com')).toBeTruthy();
expect(isUrl('http://userid:password@example.com/')).toBeTruthy();
expect(isUrl('http://142.42.1.1/')).toBeTruthy();
expect(isUrl('http://142.42.1.1:8080/')).toBeTruthy();
expect(isUrl('http://foo.com/blah_(wikipedia)#cite-1')).toBeTruthy();
expect(isUrl('http://foo.com/blah_(wikipedia)_blah#cite-1')).toBeTruthy();
expect(isUrl('http://foo.com/(something)?after=parens')).toBeTruthy();
expect(isUrl('http://code.google.com/events/#&product=browser')).toBeTruthy();
expect(isUrl('http://j.mp')).toBeTruthy();
expect(isUrl('http://foo.bar/?q=Test%20URL-encoded%20stuff')).toBeTruthy();
expect(isUrl('http://1337.net')).toBeTruthy();
expect(isUrl('http://a.b-c.de')).toBeTruthy();
expect(isUrl('http://223.255.255.254')).toBeTruthy();
expect(isUrl('https://foo_bar.example.com/')).toBeTruthy();
});

it('should not be valid', () => {
expect(isUrl('http://')).toBeFalsy();
expect(isUrl('http://?')).toBeFalsy();
expect(isUrl('http://??')).toBeFalsy();
expect(isUrl('http://??/')).toBeFalsy();
expect(isUrl('http://#')).toBeFalsy();
expect(isUrl('http://##')).toBeFalsy();
expect(isUrl('http://##/')).toBeFalsy();
expect(isUrl('//')).toBeFalsy();
expect(isUrl('//a')).toBeFalsy();
expect(isUrl('///a')).toBeFalsy();
expect(isUrl('///')).toBeFalsy();
expect(isUrl('foo.com')).toBeFalsy();
expect(isUrl('http:// shouldfail.com')).toBeFalsy();
expect(isUrl(':// should fail')).toBeFalsy();
});
});

+ 14
- 0
server/sonar-web/src/main/js/helpers/urls.ts View File

@@ -191,3 +191,17 @@ export function getHomePageUrl(homepage: HomePage) {
// should never happen, but just in case...
return '/projects';
}

export function isUrl(url: string) {
if (!URL) {
const elem = document.createElement('a');
elem.href = url;
return !!(elem.host && elem.hostname && elem.protocol);
}
try {
const parsedUrl = new URL(url);
return url.includes(parsedUrl.host);
} catch (error) {
return false;
}
}

+ 6
- 1
sonar-core/src/main/resources/org/sonar/l10n/core.properties View File

@@ -2818,6 +2818,11 @@ webhooks.create=Create Webhook
webhooks.delete=Delete Webhook
webhooks.delete.confirm=Are you sure you want to delete the webhook "{0}"?
webhooks.description=Webhooks are used to notify external services when a project analysis is done. An HTTP POST request including a JSON payload is sent to each of the provided URLs. Learn more in the {url}.
webhooks.deliveries.show=Show recent deliveries
webhooks.deliveries_for_x=Recent deliveries for {0}
webhooks.delivery.duration_x=Duration: {0}
webhooks.delivery.payload=Payload:
webhooks.delivery.response_x=Response: {0}
webhooks.documentation_link=Webhooks documentation
webhooks.maximum_reached=You reached your maximum number of {0} webhooks. You can still update or delete an existing one.
webhooks.name=Name
@@ -2825,7 +2830,7 @@ webhooks.name.required=Name is required.
webhooks.no_result=No webhook defined.
webhooks.update=Update Webhook
webhooks.url=URL
webhooks.url.bad_auth=Bad format of URL authentication.
webhooks.url.bad_format=Bad format of URL.
webhooks.url.bad_protocol=URL must start with "http://" or "https://".
webhooks.url.description=Server endpoint that will receive the webhook payload, for example: "http://my_server/foo". If HTTP Basic authentication is used, HTTPS is recommended to avoid man in the middle attacks. Example: "https://myLogin:myPassword@my_server/foo"
webhooks.url.required=URL is required.

Loading…
Cancel
Save