From: Teryk Bellahsene Date: Wed, 2 May 2018 10:00:07 +0000 (+0200) Subject: SQBILLING-93 SonarCloud notifies Muppet when an organization is deleted X-Git-Tag: 7.5~1281 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=8b16ffe330aecd26680dc46a0ba229af058132d4;p=sonarqube.git SQBILLING-93 SonarCloud notifies Muppet when an organization is deleted --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidations.java b/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidations.java index baba8d3af8c..797211a328b 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidations.java +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidations.java @@ -48,6 +48,11 @@ public interface BillingValidations { */ boolean canUpdateProjectVisibilityToPrivate(Organization organization); + /** + * Actions to do on an organization deletion + */ + void onDelete(Organization organization); + class Organization { private final String key; private final String uuid; diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidationsProxyImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidationsProxyImpl.java index f2494087d3a..717c323dd97 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidationsProxyImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/BillingValidationsProxyImpl.java @@ -55,4 +55,12 @@ public class BillingValidationsProxyImpl implements BillingValidationsProxy { public boolean canUpdateProjectVisibilityToPrivate(Organization organization) { return billingValidationsExtension == null || billingValidationsExtension.canUpdateProjectVisibilityToPrivate(organization); } + + @Override + public void onDelete(Organization organization) { + if (billingValidationsExtension == null) { + return; + } + billingValidationsExtension.onDelete(organization); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java index 3a486a15bd2..f7c8158dfcc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteAction.java @@ -35,6 +35,8 @@ import org.sonar.db.organization.OrganizationDto; import org.sonar.db.qualitygate.QualityGateDto; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.server.component.ComponentCleanerService; +import org.sonar.server.organization.BillingValidations; +import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.organization.DefaultOrganization; import org.sonar.server.organization.DefaultOrganizationProvider; import org.sonar.server.organization.OrganizationFlags; @@ -62,10 +64,11 @@ public class DeleteAction implements OrganizationsWsAction { private final UserIndexer userIndexer; private final QProfileFactory qProfileFactory; private final ProjectLifeCycleListeners projectLifeCycleListeners; + private final BillingValidationsProxy billingValidations; - public DeleteAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider, - ComponentCleanerService componentCleanerService, OrganizationFlags organizationFlags, UserIndexer userIndexer, - QProfileFactory qProfileFactory, ProjectLifeCycleListeners projectLifeCycleListeners) { + public DeleteAction(UserSession userSession, DbClient dbClient, DefaultOrganizationProvider defaultOrganizationProvider, ComponentCleanerService componentCleanerService, + OrganizationFlags organizationFlags, UserIndexer userIndexer, QProfileFactory qProfileFactory, ProjectLifeCycleListeners projectLifeCycleListeners, + BillingValidationsProxy billingValidations) { this.userSession = userSession; this.dbClient = dbClient; this.defaultOrganizationProvider = defaultOrganizationProvider; @@ -74,6 +77,7 @@ public class DeleteAction implements OrganizationsWsAction { this.userIndexer = userIndexer; this.qProfileFactory = qProfileFactory; this.projectLifeCycleListeners = projectLifeCycleListeners; + this.billingValidations = billingValidations; } @Override @@ -116,6 +120,7 @@ public class DeleteAction implements OrganizationsWsAction { deleteQualityProfiles(dbSession, organization); deleteQualityGates(dbSession, organization); deleteOrganization(dbSession, organization); + billingValidations.onDelete(new BillingValidations.Organization(organization.getKey(), organization.getUuid())); response.noContent(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java index 78f7909f20d..5790c4e44f9 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteActionTest.java @@ -59,6 +59,8 @@ import org.sonar.server.es.SearchOptions; import org.sonar.server.exceptions.ForbiddenException; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.exceptions.UnauthorizedException; +import org.sonar.server.organization.BillingValidations; +import org.sonar.server.organization.BillingValidationsProxy; import org.sonar.server.organization.TestDefaultOrganizationProvider; import org.sonar.server.organization.TestOrganizationFlags; import org.sonar.server.project.Project; @@ -119,11 +121,13 @@ public class DeleteActionTest { private final WebhookDeliveryDao deliveryDao = dbClient.webhookDeliveryDao(); private final WebhookDeliveryDbTester webhookDeliveryDbTester = db.webhookDelivery(); private ProjectLifeCycleListeners projectLifeCycleListeners = mock(ProjectLifeCycleListeners.class); + private BillingValidationsProxy billingValidationsProxy = mock(BillingValidationsProxy.class); private WsActionTester wsTester = new WsActionTester( - new DeleteAction(userSession, dbClient, defaultOrganizationProvider, spiedComponentCleanerService, organizationFlags, userIndexer, qProfileFactory, projectLifeCycleListeners)); + new DeleteAction(userSession, dbClient, defaultOrganizationProvider, spiedComponentCleanerService, organizationFlags, userIndexer, qProfileFactory, projectLifeCycleListeners, + billingValidationsProxy)); @Test - public void test_definition() { + public void definition() { WebService.Action action = wsTester.getDef(); assertThat(action.key()).isEqualTo("delete"); assertThat(action.isPost()).isTrue(); @@ -542,6 +546,16 @@ public class DeleteActionTest { } } + @Test + public void call_billing_validation_on_delete() { + OrganizationDto organization = db.organizations().insert(); + logInAsAdministrator(organization); + + sendRequest(organization); + + verify(billingValidationsProxy).onDelete(any(BillingValidations.Organization.class)); + } + @DataProvider public static Object[][] indexOfFailingProjectDeletion() { return new Object[][]{ diff --git a/server/sonar-web/src/main/js/api/organizations.ts b/server/sonar-web/src/main/js/api/organizations.ts index 943249011cd..cc321f31678 100644 --- a/server/sonar-web/src/main/js/api/organizations.ts +++ b/server/sonar-web/src/main/js/api/organizations.ts @@ -104,3 +104,20 @@ export function changeProjectVisibility( ): Promise { return post('/api/organizations/update_project_visibility', { organization, projectVisibility }); } + +export interface OrganizationBilling { + nclocCount: number; + subscription: { + plan?: { + maxNcloc: number; + price: number; + }; + nextBillingDate?: string; + status: 'active' | 'inactive' | 'suspended'; + trial: boolean; + }; +} + +export function getOrganizationBilling(organization: string): Promise { + return getJSON('/api/billing/show', { organization, p: 1, ps: 1 }); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js deleted file mode 100644 index df8a8333091..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * 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. - */ -// @flow -import React from 'react'; -import Helmet from 'react-helmet'; -import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import Modal from '../../../components/controls/Modal'; -import { translate } from '../../../helpers/l10n'; -import { getOrganizationByKey } from '../../../store/rootReducer'; -import { deleteOrganization } from '../actions'; - -class OrganizationDelete extends React.PureComponent { - /*:: props: { - organization: { - key: string, - name: string - }, - router: { - replace: string => void - }, - deleteOrganization: string => Promise<*> - }; -*/ - - state = { - deleting: false, - loading: false - }; - - handleSubmit = (e /*: Object */) => { - e.preventDefault(); - this.setState({ loading: true }); - this.props.deleteOrganization(this.props.organization.key).then(() => { - this.props.router.replace('/'); - }); - }; - - handleOpenModal = () => { - this.setState({ deleting: true }); - }; - - handleCloseModal = () => { - this.setState({ deleting: false }); - }; - - renderModal() { - return ( - -
-

{translate('organization.delete')}

-
- -
-
{translate('organization.delete.question')}
- -
- {this.state.loading ? ( - - ) : ( -
- - -
- )} -
- -
- ); - } - - render() { - const title = translate('organization.delete'); - return ( -
- - -
-

{title}

-
{translate('organization.delete.description')}
-
- -
- - {this.state.deleting && this.renderModal()} -
-
- ); - } -} - -const mapDispatchToProps = { deleteOrganization }; - -export default connect(null, mapDispatchToProps)(withRouter(OrganizationDelete)); - -export const UnconnectedOrganizationDelete = OrganizationDelete; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx new file mode 100644 index 00000000000..e68bb78f79c --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/OrganizationDelete.tsx @@ -0,0 +1,135 @@ +/* + * 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 * as PropTypes from 'prop-types'; +import Helmet from 'react-helmet'; +import { connect } from 'react-redux'; +import ConfirmButton from '../../../components/controls/ConfirmButton'; +import { translate } from '../../../helpers/l10n'; +import { deleteOrganization } from '../actions'; +import { Organization } from '../../../app/types'; +import { Button } from '../../../components/ui/buttons'; +import { getOrganizationBilling } from '../../../api/organizations'; + +interface DispatchToProps { + deleteOrganization: (key: string) => Promise; +} + +interface OwnProps { + organization: Pick; +} + +type Props = OwnProps & DispatchToProps; + +interface State { + hasPaidPlan?: boolean; +} + +export class OrganizationDelete extends React.PureComponent { + mounted = false; + static contextTypes = { + router: PropTypes.object, + onSonarCloud: PropTypes.bool + }; + + state: State = {}; + + componentDidMount() { + this.mounted = true; + this.fetchOrganizationPlanInfo(); + } + + componentWillUnmount() { + this.mounted = false; + } + + fetchOrganizationPlanInfo = () => { + if (this.context.onSonarCloud) { + getOrganizationBilling(this.props.organization.key).then( + billingInfo => { + if (this.mounted) { + this.setState({ + hasPaidPlan: billingInfo.subscription.status !== 'inactive' + }); + } + }, + () => { + if (this.mounted) { + this.setState({ hasPaidPlan: false }); + } + } + ); + } + }; + + onDelete = () => { + return this.props.deleteOrganization(this.props.organization.key).then(() => { + this.context.router.replace('/'); + }); + }; + + render() { + const { hasPaidPlan } = this.state; + const { onSonarCloud } = this.context; + const title = translate('organization.delete'); + return ( + <> + +
+
+

{title}

+
+ {onSonarCloud + ? translate('organization.delete.description.sonarcloud') + : translate('organization.delete.description')} +
+
+ + {translate('organization.delete.question')} + {hasPaidPlan && ( +

+ {translate('organization.delete.sonarcloud.paid_plan_info')} +

+ )} +
+ } + modalHeader={translate('organization.delete')} + onConfirm={this.onDelete}> + {({ onClick }) => ( + + )} + + + + ); + } +} + +const mapDispatchToProps: DispatchToProps = { deleteOrganization: deleteOrganization as any }; + +export default connect(null, mapDispatchToProps)( + OrganizationDelete +); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js deleted file mode 100644 index 1be39fb949f..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 React from 'react'; -import { shallow } from 'enzyme'; -import { UnconnectedOrganizationDelete } from '../OrganizationDelete'; - -it('smoke test', () => { - const organization = { key: 'foo', name: 'Foo' }; - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - - wrapper.setState({ deleting: true }); - expect(wrapper).toMatchSnapshot(); - - wrapper.setState({ loading: true }); - expect(wrapper).toMatchSnapshot(); -}); diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx new file mode 100644 index 00000000000..fff071d494a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/OrganizationDelete-test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { OrganizationDelete } from '../OrganizationDelete'; +import { getOrganizationBilling } from '../../../../api/organizations'; +import { waitAndUpdate } from '../../../../helpers/testUtils'; + +jest.mock('../../../../api/organizations', () => ({ + getOrganizationBilling: jest.fn(() => + Promise.resolve({ nclocCount: 1000, subscription: { status: 'active', trial: true } }) + ) +})); + +beforeEach(() => { + (getOrganizationBilling as jest.Mock).mockClear(); +}); + +it('smoke test', () => { + expect(getWrapper()).toMatchSnapshot(); +}); + +it('should redirect the page', async () => { + const deleteOrganization = jest.fn(() => Promise.resolve()); + const replace = jest.fn(); + const wrapper = getWrapper({ deleteOrganization }, { router: { replace } }); + (wrapper.instance() as OrganizationDelete).onDelete(); + await waitAndUpdate(wrapper); + expect(deleteOrganization).toHaveBeenCalledWith('foo'); + expect(replace).toHaveBeenCalledWith('/'); +}); + +it('should show a info message for paying organization', async () => { + const wrapper = getWrapper({}, { onSonarCloud: true }); + await waitAndUpdate(wrapper); + expect(getOrganizationBilling).toHaveBeenCalledWith('foo'); + expect(wrapper).toMatchSnapshot(); +}); + +function getWrapper(props = {}, context = {}) { + return shallow( + Promise.resolve())} + organization={{ key: 'foo', name: 'Foo' }} + {...props} + />, + + { context: { router: { replace: jest.fn() }, ...context } } + ); +} diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap deleted file mode 100644 index d8593ee9ffc..00000000000 --- a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.js.snap +++ /dev/null @@ -1,174 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`smoke test 1`] = ` -
- -
-

- organization.delete -

-
- organization.delete.description -
-
-
- -
-
-`; - -exports[`smoke test 2`] = ` -
- -
-

- organization.delete -

-
- organization.delete.description -
-
-
- - -
-

- organization.delete -

-
-
-
- organization.delete.question -
-
-
- - -
-
-
-
-
-
-`; - -exports[`smoke test 3`] = ` -
- -
-

- organization.delete -

-
- organization.delete.description -
-
-
- - -
-

- organization.delete -

-
-
-
- organization.delete.question -
-
- -
- -
-
-
-`; diff --git a/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap new file mode 100644 index 00000000000..0f7e0df211a --- /dev/null +++ b/server/sonar-web/src/main/js/apps/organizations/components/__tests__/__snapshots__/OrganizationDelete-test.tsx.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should show a info message for paying organization 1`] = ` + + +
+
+

+ organization.delete +

+
+ organization.delete.description.sonarcloud +
+
+ + organization.delete.question +

+ organization.delete.sonarcloud.paid_plan_info +

+
+ } + modalHeader="organization.delete" + onConfirm={[Function]} + /> + +
+`; + +exports[`smoke test 1`] = ` + + +
+
+

+ organization.delete +

+
+ organization.delete.description +
+
+ + organization.delete.question +
+ } + modalHeader="organization.delete" + onConfirm={[Function]} + /> + +
+`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 622cfbdde75..d75b929ec54 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -2517,6 +2517,8 @@ organization.avatar.preview=Preview organization.created=Organization "{0}" has been created. organization.delete=Delete Organization organization.delete.description=Delete this organization from SonarQube. All projects belonging to the organization will be deleted as well. The operation cannot be undone. +organization.delete.description.sonarcloud=Delete this organization from SonarCloud. All projects belonging to the organization will be deleted as well. The operation cannot be undone. +organization.delete.sonarcloud.paid_plan_info=Your current paid plan subscription will stop and you won't be charged anymore. organization.delete.question=Are you sure you want to delete this organization? organization.deleted=Organization has been deleted. organization.description=Description diff --git a/tests/plugins/fake-billing-plugin/src/main/java/FakeBillingValidations.java b/tests/plugins/fake-billing-plugin/src/main/java/FakeBillingValidations.java index bfd4315f6d2..16c178c4233 100644 --- a/tests/plugins/fake-billing-plugin/src/main/java/FakeBillingValidations.java +++ b/tests/plugins/fake-billing-plugin/src/main/java/FakeBillingValidations.java @@ -77,4 +77,9 @@ public class FakeBillingValidations implements BillingValidationsExtension { } return !settings.getBoolean(PREVENT_UPDATING_PROJECTS_VISIBILITY_TO_PRIVATE_SETTING); } + + @Override + public void onDelete(Organization organization) { + // do nothing + } }