]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21179 Use update endpoint when reactivating a rule
authorstanislavh <stanislav.honcharov@sonarsource.com>
Wed, 6 Dec 2023 10:08:25 +0000 (11:08 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 8 Dec 2023 20:03:05 +0000 (20:03 +0000)
server/sonar-web/src/main/js/api/mocks/CodingRulesServiceMock.ts
server/sonar-web/src/main/js/api/mocks/data/ids.ts
server/sonar-web/src/main/js/api/mocks/data/rules.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CustomRule-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx
server/sonar-web/src/main/js/apps/coding-rules/utils-tests.tsx
server/sonar-web/src/main/js/queries/rules.ts

index e7f50d935295885f296c67d9c3ae887d36f91266..d0c9612a95a0a6c5f74eda22d14018684a962161 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { HttpStatusCode } from 'axios';
 import { cloneDeep, countBy, pick, trim } from 'lodash';
 import { RuleDescriptionSections } from '../../apps/coding-rules/rule';
 import { getStandards } from '../../helpers/security-standard';
@@ -30,7 +31,7 @@ import {
 import { RuleRepository, SearchRulesResponse } from '../../types/coding-rules';
 import { ComponentQualifier, Visibility } from '../../types/component';
 import { RawIssuesResponse } from '../../types/issues';
-import { SearchRulesQuery } from '../../types/rules';
+import { RuleStatus, SearchRulesQuery } from '../../types/rules';
 import { SecurityStandard } from '../../types/security';
 import { Dict, Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../types/types';
 import { NoticeType } from '../../types/users';
@@ -174,7 +175,7 @@ export default class CodingRulesServiceMock {
     cwe,
     activation,
   }: FacetFilter) {
-    let filteredRules = this.rules;
+    let filteredRules = this.getRulesFilteredByRemovedStatus();
     if (cleanCodeAttributeCategories) {
       filteredRules = filteredRules.filter(
         (r) =>
@@ -266,12 +267,16 @@ export default class CodingRulesServiceMock {
     this.rulesActivations = mockRulesActivationsInQP();
   }
 
+  getRulesFilteredByRemovedStatus() {
+    return this.rules.filter((r) => r.status !== RuleStatus.Removed);
+  }
+
   allRulesCount() {
-    return this.rules.length;
+    return this.getRulesFilteredByRemovedStatus().length;
   }
 
   allRulesName() {
-    return this.rules.map((r) => r.name);
+    return this.getRulesFilteredByRemovedStatus().map((r) => r.name);
   }
 
   allQualityProfile(language: string) {
@@ -322,12 +327,14 @@ export default class CodingRulesServiceMock {
   };
 
   handleUpdateRule = (data: RulesUpdateRequest): Promise<RuleDetails> => {
-    const rule = this.rules.find((r) => r.key === data.key);
+    // find rule if key is in the list of rules or key is a part of 'repo:key'
+    const rule = this.rules.find((r) => data.key.split(':').some((part) => part === r.key));
     if (rule === undefined) {
       return Promise.reject({
         errors: [{ msg: `No rule has been found for id ${data.key}` }],
       });
     }
+
     const template = this.rules.find((r) => r.key === rule.templateKey);
 
     // Lets not convert the md to html in test.
@@ -337,6 +344,7 @@ export default class CodingRulesServiceMock {
     rule.mdNote = data.markdown_note !== undefined ? data.markdown_note : rule.mdNote;
     rule.htmlNote = data.markdown_note !== undefined ? data.markdown_note : rule.htmlNote;
     rule.name = data.name !== undefined ? data.name : rule.name;
+    rule.status = rule.status === RuleStatus.Removed ? RuleStatus.Ready : rule.status;
     if (template && data.params) {
       rule.params = [];
       data.params.split(';').forEach((param) => {
@@ -377,6 +385,27 @@ export default class CodingRulesServiceMock {
         }) ?? [],
     });
 
+    const rulesFromTemplateWithSameKeys = this.rules.filter(
+      (rule) => rule.templateKey === newRule.templateKey && rule.key === newRule.key,
+    );
+
+    if (rulesFromTemplateWithSameKeys.length > 0) {
+      if (
+        rulesFromTemplateWithSameKeys.find(
+          (rule) => rule.status === RuleStatus.Removed && rule.key === newRule.key,
+        )
+      ) {
+        return Promise.reject({
+          status: HttpStatusCode.Conflict,
+          errors: [{ msg: `Rule with the same was removed before` }],
+        });
+      }
+
+      return Promise.reject({
+        errors: [{ msg: `A rule with key ${newRule.key} already exists` }],
+      });
+    }
+
     this.rules.push(newRule);
 
     return this.reply(newRule);
@@ -469,6 +498,7 @@ export default class CodingRulesServiceMock {
         activation,
       });
     }
+
     const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
     return this.reply({
       actives: qprofile ? this.rulesActivations : undefined,
@@ -485,13 +515,13 @@ export default class CodingRulesServiceMock {
   handleBulkActivateRules = () => {
     if (this.applyWithWarning) {
       return this.reply({
-        succeeded: this.rules.length - 1,
+        succeeded: this.getRulesFilteredByRemovedStatus().length - 1,
         failed: 1,
         errors: [{ msg: 'c rule c:S6069 cannot be activated on cpp profile SonarSource' }],
       });
     }
     return this.reply({
-      succeeded: this.rules.length,
+      succeeded: this.getRulesFilteredByRemovedStatus().length,
       failed: 0,
       errors: [],
     });
@@ -499,7 +529,7 @@ export default class CodingRulesServiceMock {
 
   handleBulkDeactivateRules = () => {
     return this.reply({
-      succeeded: this.rules.length,
+      succeeded: this.getRulesFilteredByRemovedStatus().length,
       failed: 0,
     });
   };
index f680b17e2a83fae179f4ad7cc1cb498fd481746a..502e5600ffdab991f513ff312b9d1c9745ba72e3 100644 (file)
@@ -50,6 +50,7 @@ export const RULE_8 = 'rule8';
 export const RULE_9 = 'rule9';
 export const RULE_10 = 'rule10';
 export const RULE_11 = 'rule11';
+export const RULE_12 = 'rule12';
 
 // Quality Profiles.
 export const QP_1 = 'p1';
index f7a1aca3dbf89a34561b77f5e3bbdb34121bd064..3616cb845e41306ff45326548267b0ec7d39bc25 100644 (file)
@@ -25,6 +25,7 @@ import {
   SoftwareImpactSeverity,
   SoftwareQuality,
 } from '../../../types/clean-code-taxonomy';
+import { RuleStatus } from '../../../types/rules';
 import {
   ADVANCED_RULE,
   QP_1,
@@ -34,6 +35,7 @@ import {
   RULE_1,
   RULE_10,
   RULE_11,
+  RULE_12,
   RULE_2,
   RULE_3,
   RULE_4,
@@ -214,7 +216,7 @@ export function mockRuleDetailsList() {
         { key: '1', type: 'TEXT', htmlDesc: 'html description for key 1' },
         { key: '2', type: 'NUMBER', defaultValue: 'default value for key 2' },
       ],
-      templateKey: 'rule8',
+      templateKey: RULE_8,
     }),
     mockRuleDetails({
       createdAt: '2022-12-16T17:26:54+0100',
@@ -242,6 +244,17 @@ export function mockRuleDetailsList() {
       langName: 'Java',
       name: 'Common java rule',
     }),
+    mockRuleDetails({
+      key: RULE_12,
+      type: 'BUG',
+      severity: 'MINOR',
+      lang: 'py',
+      langName: 'Python',
+      tags: ['awesome'],
+      name: 'Deleted custom rule based on rule8',
+      templateKey: RULE_8,
+      status: RuleStatus.Removed,
+    }),
   ];
 }
 
index 850c4314fed62ac66df198218e7d47383a9d1135..7533d1f4810d9e468d94d2817577113c297b85b8 100644 (file)
@@ -21,6 +21,7 @@ import selectEvent from 'react-select-event';
 import CodingRulesServiceMock from '../../../api/mocks/CodingRulesServiceMock';
 import SettingsServiceMock from '../../../api/mocks/SettingsServiceMock';
 import { mockLoggedInUser } from '../../../helpers/testMocks';
+import { byText } from '../../../helpers/testSelector';
 import { SoftwareQuality } from '../../../types/clean-code-taxonomy';
 import { getPageObjects, renderCodingRulesApp } from '../utils-tests';
 
@@ -101,6 +102,29 @@ describe('custom rule', () => {
     expect(ui.customRuleItemLink('New Custom Rule').get()).toBeInTheDocument();
   });
 
+  it('can reactivate custom rule', async () => {
+    const { ui, user } = getPageObjects();
+    rulesHandler.setIsAdmin();
+    renderCodingRulesApp(mockLoggedInUser(), 'coding_rules?open=rule8');
+    await ui.detailsloaded();
+
+    // Try create custom rule with existing rule with removed status
+    await user.click(ui.createCustomRuleButton.get());
+    await user.type(ui.ruleNameTextbox.get(), 'Reactivate custom Rule');
+    await user.clear(ui.keyTextbox.get());
+    await user.type(ui.keyTextbox.get(), 'rule12');
+    await user.type(ui.descriptionTextbox.get(), 'Some description for custom rule');
+
+    await user.click(ui.createButton.get());
+
+    expect(byText('coding_rules.reactivate.help').get()).toBeInTheDocument();
+
+    // Reactivate rule
+    await user.click(ui.reactivateButton.get());
+    // Verify the rule is reactivated
+    expect(ui.customRuleItemLink('Reactivate custom Rule').get()).toBeInTheDocument();
+  });
+
   it('can edit custom rule', async () => {
     const { ui, user } = getPageObjects();
     rulesHandler.setIsAdmin();
index dce99ab2ba7059f2ec7b9dfb51d4b8465907e434..87d3c2dd768583f8d7b0b0317a8dc1dc39a7e5e4 100644 (file)
@@ -74,17 +74,22 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
     templateRule.cleanCodeAttribute ?? CleanCodeAttribute.Conventional,
   );
   const [impacts, setImpacts] = React.useState<SoftwareImpact[]>(templateRule?.impacts ?? []);
-  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(props.onClose);
+  const customRulesSearchParams = {
+    f: 'name,severity,params',
+    template_key: templateRule.key,
+  };
+  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(
+    customRulesSearchParams,
+    props.onClose,
+  );
   const { mutate: createRule, isLoading: creatingRule } = useCreateRuleMutation(
-    {
-      f: 'name,severity,params',
-      template_key: templateRule.key,
-    },
+    customRulesSearchParams,
     props.onClose,
     (response: Response) => {
       setReactivating(response.status === HttpStatusCode.Conflict);
     },
   );
+  const warningRef = React.useRef<HTMLDivElement>(null);
 
   const submitting = updatingRule || creatingRule;
   const hasError = impacts.length === 0;
@@ -99,18 +104,41 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
       params: stringifiedParams,
       status,
     };
-    return customRule
-      ? updateRule({ ...ruleData, key: customRule.key })
-      : createRule({
-          ...ruleData,
-          customKey: key,
-          preventReactivation: !reactivating,
-          templateKey: templateRule.key,
-          cleanCodeAttribute: ccAttribute,
-          impacts,
-        });
+
+    if (customRule) {
+      updateRule({
+        ...ruleData,
+        key: customRule.key,
+      });
+    } else if (reactivating) {
+      updateRule({
+        ...ruleData,
+        key: `${templateRule.repo}:${key}`,
+      });
+    } else {
+      createRule({
+        ...ruleData,
+        customKey: key,
+        templateKey: templateRule.key,
+        preventReactivation: true,
+        cleanCodeAttribute: ccAttribute,
+        impacts,
+      });
+    }
   };
 
+  // If key changes, then most likely user did it to create a new rule instead of reactivating one
+  React.useEffect(() => {
+    setReactivating(false);
+  }, [key]);
+
+  // scroll to warning when it appears
+  React.useEffect(() => {
+    if (reactivating) {
+      warningRef.current?.scrollIntoView({ behavior: 'smooth' });
+    }
+  }, [reactivating]);
+
   const NameField = React.useMemo(
     () => (
       <FormField
@@ -298,9 +326,11 @@ export default function CustomRuleFormModal(props: Readonly<Props>) {
           }}
         >
           {reactivating && (
-            <FlagMessage variant="warning" className="sw-mb-6">
-              {translate('coding_rules.reactivate.help')}
-            </FlagMessage>
+            <div ref={warningRef}>
+              <FlagMessage variant="warning" className="sw-mb-6">
+                {translate('coding_rules.reactivate.help')}
+              </FlagMessage>
+            </div>
           )}
 
           <MandatoryFieldsExplanation className="sw-mb-4" />
index f3357a6a7939401674965fd997fe03946dbd14e8..22905f3f68f94dc701892a20d7ab69be21a967c0 100644 (file)
@@ -47,7 +47,7 @@ export default function RuleDetailsDescription(props: Readonly<Props>) {
   const [descriptionForm, setDescriptionForm] = React.useState(false);
   const [removeDescriptionModal, setDescriptionModal] = React.useState(false);
 
-  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(() =>
+  const { mutate: updateRule, isLoading: updatingRule } = useUpdateRuleMutation(undefined, () =>
     setDescriptionForm(false),
   );
 
index 1bb89ffafb7cfc8be15ccf19bac2587abf992555..9e149265f6b659b9fff15d4375d2efd8c9a4e119 100644 (file)
@@ -191,6 +191,7 @@ const selectors = {
   statusSelect: byRole('combobox', { name: 'coding_rules.filters.status' }),
   descriptionTextbox: byRole('textbox', { name: 'description' }),
   createButton: byRole('button', { name: 'create' }),
+  reactivateButton: byRole('button', { name: 'coding_rules.reactivate' }),
   deleteButton: byRole('button', { name: 'delete' }),
 };
 
index b7c303b2e67b9667381b95035b7b121e77c1763a..22e03e2f25700b9b567eb515d19aecb5970c3edf 100644 (file)
@@ -74,13 +74,22 @@ export function useCreateRuleMutation(
   });
 }
 
-export function useUpdateRuleMutation(onSuccess?: (rule: RuleDetails) => unknown) {
+export function useUpdateRuleMutation(
+  searchQuery?: SearchRulesQuery,
+  onSuccess?: (rule: RuleDetails) => unknown,
+) {
   const queryClient = useQueryClient();
 
   return useMutation({
     mutationFn: updateRule,
     onSuccess: (rule) => {
       onSuccess?.(rule);
+      queryClient.setQueryData<SearchRulesResponse>(
+        getRulesQueryKey('search', searchQuery),
+        (oldData) => {
+          return oldData ? { ...oldData, rules: [rule, ...oldData.rules] } : undefined;
+        },
+      );
       queryClient.setQueryData<{ actives?: RuleActivation[]; rule: RuleDetails }>(
         getRulesQueryKey('details', rule.key),
         (oldData) => {