aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/DbVersion95.java6
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSections.java5
-rw-r--r--server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptions.java193
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSectionsTest.java18
-rw-r--r--server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest.java266
-rw-r--r--server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest/schema.sql35
6 files changed, 509 insertions, 14 deletions
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/DbVersion95.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/DbVersion95.java
index 2a3661ac40b..e2d0ac509b2 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/DbVersion95.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/DbVersion95.java
@@ -23,6 +23,8 @@ import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
import org.sonar.server.platform.db.migration.version.DbVersion;
public class DbVersion95 implements DbVersion {
+ static final String DEFAULT_DESCRIPTION_KEY = "default";
+
@Override
public void addSteps(MigrationStepRegistry registry) {
registry
@@ -41,6 +43,8 @@ public class DbVersion95 implements DbVersion {
.add(6412, "Add rules_metadata columns to rules table", AddRulesMetadataColumnsToRulesTable.class)
.add(6413, "Populate rules metadata in rules table", PopulateRulesMetadataInRuleTable.class)
.add(6414, "Drop rules_metadata table", DropRuleMetadataTable.class)
- ;
+
+ .add(6415, "Migrate hotspot rule descriptions", MigrateHotspotRuleDescriptions.class)
+ ;
}
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSections.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSections.java
index 97ab8896093..8b8c486b1c8 100644
--- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSections.java
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSections.java
@@ -19,7 +19,6 @@
*/
package org.sonar.server.platform.db.migration.version.v95;
-import com.google.common.annotations.VisibleForTesting;
import java.sql.SQLException;
import java.util.List;
import org.sonar.core.util.UuidFactory;
@@ -31,8 +30,6 @@ import org.sonar.server.platform.db.migration.step.Upsert;
import static org.sonar.server.platform.db.migration.version.v95.CreateRuleDescSectionsTable.RULE_DESCRIPTION_SECTIONS_TABLE;
public class InsertRuleDescriptionIntoRuleDescSections extends DataChange {
- @VisibleForTesting
- static final String DEFAULT_DESCRIPTION_KEY = "default";
private static final String SELECT_EXISTING_RULE_DESCRIPTIONS = "select uuid, description from rules where description is not null "
+ "and uuid not in (select rule_uuid from " + RULE_DESCRIPTION_SECTIONS_TABLE + ")";
@@ -71,7 +68,7 @@ public class InsertRuleDescriptionIntoRuleDescSections extends DataChange {
insertRuleDescSections
.setString(1, uuidFactory.create())
.setString(2, ruleDb.getUuid())
- .setString(3, DEFAULT_DESCRIPTION_KEY)
+ .setString(3, DbVersion95.DEFAULT_DESCRIPTION_KEY)
.setString(4, ruleDb.getDescription())
.addBatch();
}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptions.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptions.java
new file mode 100644
index 00000000000..1ac7e55bc33
--- /dev/null
+++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptions.java
@@ -0,0 +1,193 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.
+ */
+package org.sonar.server.platform.db.migration.version.v95;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.CheckForNull;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.step.DataChange;
+import org.sonar.server.platform.db.migration.step.Upsert;
+
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ASSESS_THE_PROBLEM_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.HOW_TO_FIX_SECTION_KEY;
+import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
+import static org.sonar.server.platform.db.migration.version.v95.CreateRuleDescSectionsTable.RULE_DESCRIPTION_SECTIONS_TABLE;
+import static org.sonar.server.platform.db.migration.version.v95.DbVersion95.DEFAULT_DESCRIPTION_KEY;
+
+public class MigrateHotspotRuleDescriptions extends DataChange {
+
+ private static final String SELECT_DEFAULT_HOTSPOTS_DESCRIPTIONS = "select r.uuid, rds.uuid, rds.content from rules r \n"
+ + "left join " + RULE_DESCRIPTION_SECTIONS_TABLE + " rds on r.uuid = rds.rule_uuid \n"
+ + "where r.rule_type = 4 and r.template_uuid is null and rds.kee = '" + DEFAULT_DESCRIPTION_KEY + "'";
+
+ private static final String INSERT_INTO_RULE_DESC_SECTIONS = "insert into " + RULE_DESCRIPTION_SECTIONS_TABLE + " (uuid, rule_uuid, kee, content) values "
+ + "(?,?,?,?)";
+ private static final String DELETE_DEFAULT_RULE_DESC_SECTIONS = "delete from " + RULE_DESCRIPTION_SECTIONS_TABLE + " where uuid = ? ";
+
+ private final UuidFactory uuidFactory;
+
+ public MigrateHotspotRuleDescriptions(Database db, UuidFactory uuidFactory) {
+ super(db);
+ this.uuidFactory = uuidFactory;
+ }
+
+ @Override
+ protected void execute(Context context) throws SQLException {
+ List<RuleDescriptionSection> selectRuleDescriptionSection = findExistingRuleDescriptions(context);
+ if (selectRuleDescriptionSection.isEmpty()) {
+ return;
+ }
+ insertRuleDescSections(context, selectRuleDescriptionSection);
+ }
+
+ private static List<RuleDescriptionSection> findExistingRuleDescriptions(Context context) throws SQLException {
+ return context.prepareSelect(SELECT_DEFAULT_HOTSPOTS_DESCRIPTIONS)
+ .list(r -> new RuleDescriptionSection(r.getString(1), r.getString(2), r.getString(3)));
+ }
+
+ private void insertRuleDescSections(Context context, List<RuleDescriptionSection> defaultRuleDescriptionSections) throws SQLException {
+ Upsert insertRuleDescSectionsQuery = context.prepareUpsert(INSERT_INTO_RULE_DESC_SECTIONS);
+ Upsert deleteQuery = context.prepareUpsert(DELETE_DEFAULT_RULE_DESC_SECTIONS);
+ for (RuleDescriptionSection ruleDescriptionSection : defaultRuleDescriptionSections) {
+ Map<String, String> sections = generateNewNamedSections(ruleDescriptionSection.getContent());
+ if (sections.isEmpty()) {
+ continue;
+ }
+ insertNewNamedSections(insertRuleDescSectionsQuery, ruleDescriptionSection.getRuleUuid(), sections);
+ deleteOldDefaultSection(deleteQuery, ruleDescriptionSection.getSectionUuid());
+ }
+ insertRuleDescSectionsQuery.execute();
+ deleteQuery.execute().commit();
+ }
+
+ private static Map<String, String> generateNewNamedSections(String descriptionInHtml) {
+ String[] split = extractSection("", descriptionInHtml);
+ String remainingText = split[0];
+ String ruleDescriptionSection = split[1];
+
+ split = extractSection("<h2>Exceptions</h2>", remainingText);
+ remainingText = split[0];
+ String exceptions = split[1];
+
+ split = extractSection("<h2>Ask Yourself Whether</h2>", remainingText);
+ remainingText = split[0];
+ String askSection = split[1];
+
+ split = extractSection("<h2>Sensitive Code Example</h2>", remainingText);
+ remainingText = split[0];
+ String sensitiveSection = split[1];
+
+ split = extractSection("<h2>Noncompliant Code Example</h2>", remainingText);
+ remainingText = split[0];
+ String noncompliantSection = split[1];
+
+ split = extractSection("<h2>Recommended Secure Coding Practices</h2>", remainingText);
+ remainingText = split[0];
+ String recommendedSection = split[1];
+
+ split = extractSection("<h2>Compliant Solution</h2>", remainingText);
+ remainingText = split[0];
+ String compliantSection = split[1];
+
+ split = extractSection("<h2>See</h2>", remainingText);
+ remainingText = split[0];
+ String seeSection = split[1];
+
+ Map<String, String> keysToContent = new HashMap<>();
+ Optional.ofNullable(createSection(ruleDescriptionSection, exceptions, remainingText)).ifPresent(d -> keysToContent.put(ROOT_CAUSE_SECTION_KEY, d));
+ Optional.ofNullable(createSection(askSection, sensitiveSection, noncompliantSection)).ifPresent(d -> keysToContent.put(ASSESS_THE_PROBLEM_SECTION_KEY, d));
+ Optional.ofNullable(createSection(recommendedSection, compliantSection, seeSection)).ifPresent(d -> keysToContent.put(HOW_TO_FIX_SECTION_KEY, d));
+ return keysToContent;
+ }
+
+ private void insertNewNamedSections(Upsert insertRuleDescSections, String ruleUuid, Map<String, String> sections) throws SQLException {
+ for (Map.Entry<String, String> sectionKeyToContent : sections.entrySet()) {
+ insertRuleDescSections
+ .setString(1, uuidFactory.create())
+ .setString(2, ruleUuid)
+ .setString(3, sectionKeyToContent.getKey())
+ .setString(4, sectionKeyToContent.getValue())
+ .addBatch();
+ }
+ }
+
+ private static void deleteOldDefaultSection(Upsert delete, String sectionUuid) throws SQLException {
+ delete
+ .setString(1, sectionUuid)
+ .addBatch();
+ }
+
+ private static String[] extractSection(String beginning, String description) {
+ String endSection = "<h2>";
+ int beginningIndex = description.indexOf(beginning);
+ if (beginningIndex != -1) {
+ int endIndex = description.indexOf(endSection, beginningIndex + beginning.length());
+ if (endIndex == -1) {
+ endIndex = description.length();
+ }
+ return new String[] {
+ description.substring(0, beginningIndex) + description.substring(endIndex),
+ description.substring(beginningIndex, endIndex)
+ };
+ } else {
+ return new String[] {description, ""};
+ }
+
+ }
+
+ @CheckForNull
+ private static String createSection(String... contentPieces) {
+ return trimToNull(String.join("", contentPieces));
+ }
+
+ @CheckForNull
+ private static String trimToNull(String input) {
+ return input.isEmpty() ? null : input;
+ }
+
+ private static class RuleDescriptionSection {
+ private final String ruleUuid;
+ private final String sectionUuid;
+ private final String content;
+
+ private RuleDescriptionSection(String ruleUuid, String sectionUuid, String content) {
+ this.ruleUuid = ruleUuid;
+ this.sectionUuid = sectionUuid;
+ this.content = content;
+ }
+
+ public String getRuleUuid() {
+ return ruleUuid;
+ }
+
+ public String getSectionUuid() {
+ return sectionUuid;
+ }
+
+ public String getContent() {
+ return content;
+ }
+ }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSectionsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSectionsTest.java
index 69c018e7389..9dbffdad1ca 100644
--- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSectionsTest.java
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/InsertRuleDescriptionIntoRuleDescSectionsTest.java
@@ -33,7 +33,7 @@ import org.sonar.server.platform.db.migration.step.DataChange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.sonar.server.platform.db.migration.version.v95.CreateRuleDescSectionsTable.RULE_DESCRIPTION_SECTIONS_TABLE;
-import static org.sonar.server.platform.db.migration.version.v95.InsertRuleDescriptionIntoRuleDescSections.DEFAULT_DESCRIPTION_KEY;
+import static org.sonar.server.platform.db.migration.version.v95.DbVersion95.DEFAULT_DESCRIPTION_KEY;
public class InsertRuleDescriptionIntoRuleDescSectionsTest {
@@ -70,31 +70,31 @@ public class InsertRuleDescriptionIntoRuleDescSectionsTest {
@Test
public void insertRuleDescriptions_whenReentrant_doesNotFail() throws SQLException {
String description1 = RandomStringUtils.randomAlphanumeric(5000);
- String uuid1 = "uuid1";
- insertRule(uuid1, description1);
+ String uuid = "uuid1";
+ insertRule(uuid, description1);
insertRuleDescriptions.execute();
insertRuleDescriptions.execute();
insertRuleDescriptions.execute();
assertThat(db.countRowsOfTable(RULE_DESCRIPTION_SECTIONS_TABLE)).isEqualTo(1);
- assertRuleDescriptionCreated(uuid1, description1);
+ assertRuleDescriptionCreated(uuid, description1);
}
@Test
public void insertRuleDescriptions_whenNoDescription_doesNotCreateRuleDescriptionSection() throws SQLException {
- String uuid1 = "uuid1";
- insertRule(uuid1, null);
+ String uuid = "uuid1";
+ insertRule(uuid, null);
insertRuleDescriptions.execute();
assertThat(db.countRowsOfTable(RULE_DESCRIPTION_SECTIONS_TABLE)).isZero();
}
- private void assertRuleDescriptionCreated(String uuid1, String description1) {
- Map<String, Object> result1 = findRuleSectionDescription(uuid1);
+ private void assertRuleDescriptionCreated(String uuid, String description1) {
+ Map<String, Object> result1 = findRuleSectionDescription(uuid);
assertThat(result1)
- .containsEntry("RULE_UUID", uuid1)
+ .containsEntry("RULE_UUID", uuid)
.containsEntry("KEE", DEFAULT_DESCRIPTION_KEY)
.containsEntry("CONTENT", description1)
.extractingByKey("UUID").isNotNull();
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest.java
new file mode 100644
index 00000000000..1e5d742c217
--- /dev/null
+++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest.java
@@ -0,0 +1,266 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.
+ */
+package org.sonar.server.platform.db.migration.version.v95;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import kotlin.Pair;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.rules.RuleType;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.UuidFactoryFast;
+import org.sonar.db.CoreDbTester;
+import org.sonar.server.platform.db.migration.step.DataChange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.sonar.server.platform.db.migration.version.v95.CreateRuleDescSectionsTable.RULE_DESCRIPTION_SECTIONS_TABLE;
+import static org.sonar.server.platform.db.migration.version.v95.DbVersion95.DEFAULT_DESCRIPTION_KEY;
+
+public class MigrateHotspotRuleDescriptionsTest {
+ private static final String LEGACY_HOTSPOT_RULE_HTML_DESC = "<p>Formatted SQL queries can be difficult to maintain"
+ + "<h2>Ask Yourself Whether</h2>\n"
+ + "balbalblabla\n"
+ + "<h2>Recommended Secure Coding Practices</h2>\n"
+ + "Consider using ORM frameworks"
+ + "<h2>Sensitive Code Example</h2>\n"
+ + "mysql\n"
+ + "<h2>Compliant Solution</h2>\n"
+ + "compliant solution desc\n"
+ + "<h2>Exceptions</h2>\n"
+ + "<p>This rule current implementation does not follow variables.</p>\n"
+ + "<h2>See</h2>\n"
+ + "<a href=\"https://owasp.org/Top10/A03_2021-Injection/\">OWASP Top 10 2021 Category A3</a> - Injection </li>\n";
+
+ private static final String ISSUE_RULE = "rule_non_hotspot";
+ private static final String LEGACY_HOTSPOT_RULE = "rule_legacy_hotspot";
+ private static final String LEGACY_HOTSPOT_CUSTOM_RULE = "rule_legacy_hotspot_custom";
+ private static final String ADVANCED_RULE = "rule_advanced_hotspot";
+
+ @Rule
+ public final CoreDbTester db = CoreDbTester.createForSchema(MigrateHotspotRuleDescriptionsTest.class, "schema.sql");
+
+ private final UuidFactory uuidFactory = UuidFactoryFast.getInstance();
+
+ private final DataChange fixHotspotRuleDescriptions = new MigrateHotspotRuleDescriptions(db.database(), uuidFactory);
+
+ @Before
+ public void setUp() {
+ insertRule(ISSUE_RULE, RuleType.CODE_SMELL);
+ insertRule(LEGACY_HOTSPOT_RULE, RuleType.SECURITY_HOTSPOT);
+ insertRule(LEGACY_HOTSPOT_CUSTOM_RULE, RuleType.SECURITY_HOTSPOT, new Pair<>("template_uuid", LEGACY_HOTSPOT_RULE));
+ insertRule(ADVANCED_RULE, RuleType.SECURITY_HOTSPOT);
+ }
+
+ private void insertRule(String uuid, RuleType ruleType, Pair<String, Object> ... additionalKeyValues) {
+ Map<String, Object> ruleParams = new HashMap<>();
+ ruleParams.put("uuid", uuid);
+ ruleParams.put("plugin_rule_key", "plugin_key_" + uuid);
+ ruleParams.put("plugin_name", "plugin_name");
+ ruleParams.put("scope", "ALL");
+ ruleParams.put("is_template", false);
+ ruleParams.put("is_external", true);
+ ruleParams.put("is_ad_hoc", false);
+ ruleParams.put("rule_type", ruleType.getDbConstant());
+ Arrays.stream(additionalKeyValues).forEach(pair -> ruleParams.put(pair.getFirst(), pair.getSecond()));
+
+ db.executeInsert("rules", ruleParams);
+ }
+
+ @Test
+ public void insertRuleDescriptions_doesNotFailIfRulesDescSectionsTableIsEmpty() {
+ assertThatCode(fixHotspotRuleDescriptions::execute)
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenHtmlDescriptionIsComplete_createAllSectionsForLegacyHotspot() throws SQLException {
+ insertSectionForLegacyHotspotRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSections = findRuleDescriptionSections(LEGACY_HOTSPOT_RULE);
+
+ assertThat(ruleDescriptionSections).hasSize(3)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("root_cause", "<p>Formatted SQL queries can be difficult to maintain<h2>Exceptions</h2>\n"
+ + "<p>This rule current implementation does not follow variables.</p>\n"))
+ .contains(tuple("assess_the_problem", "<h2>Ask Yourself Whether</h2>\n"
+ + "balbalblabla\n"
+ + "<h2>Sensitive Code Example</h2>\n"
+ + "mysql\n"))
+ .contains(tuple("how_to_fix", "<h2>Recommended Secure Coding Practices</h2>\n"
+ + "Consider using ORM frameworks<h2>Compliant Solution</h2>\n"
+ + "compliant solution desc\n"
+ + "<h2>See</h2>\n"
+ + "<a href=\"https://owasp.org/Top10/A03_2021-Injection/\">OWASP Top 10 2021 Category A3</a> - Injection </li>\n"));
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenCustomRule_doNotCreateSections() throws SQLException {
+ insertSectionForLegacyCustomHotspotRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSections = findRuleDescriptionSections(LEGACY_HOTSPOT_CUSTOM_RULE);
+
+ assertThat(ruleDescriptionSections)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .containsOnly(tuple(DEFAULT_DESCRIPTION_KEY, LEGACY_HOTSPOT_RULE_HTML_DESC));
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenMixtureOfRules_createAllSectionsForLegacyHotspotAndDoNotModifyOthers() throws SQLException {
+ insertSectionForLegacyIssueRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+ insertSectionForLegacyHotspotRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+ insertSectionForAdvancedRule("test", LEGACY_HOTSPOT_RULE_HTML_DESC);
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSectionsLegacyHotspotRule = findRuleDescriptionSections(LEGACY_HOTSPOT_RULE);
+ List<Map<String, Object>> ruleDescriptionSectionsIssueRule = findRuleDescriptionSections(ISSUE_RULE);
+ List<Map<String, Object>> ruleDescriptionSectionsAdvancedRule = findRuleDescriptionSections(ADVANCED_RULE);
+
+ assertThat(ruleDescriptionSectionsIssueRule)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .containsOnly(tuple(DEFAULT_DESCRIPTION_KEY, LEGACY_HOTSPOT_RULE_HTML_DESC));
+
+ assertThat(ruleDescriptionSectionsAdvancedRule)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("test", LEGACY_HOTSPOT_RULE_HTML_DESC));
+
+ assertThat(ruleDescriptionSectionsLegacyHotspotRule).hasSize(3)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("root_cause", "<p>Formatted SQL queries can be difficult to maintain<h2>Exceptions</h2>\n"
+ + "<p>This rule current implementation does not follow variables.</p>\n"))
+ .contains(tuple("assess_the_problem", "<h2>Ask Yourself Whether</h2>\n"
+ + "balbalblabla\n"
+ + "<h2>Sensitive Code Example</h2>\n"
+ + "mysql\n"))
+ .contains(tuple("how_to_fix", "<h2>Recommended Secure Coding Practices</h2>\n"
+ + "Consider using ORM frameworks<h2>Compliant Solution</h2>\n"
+ + "compliant solution desc\n"
+ + "<h2>See</h2>\n"
+ + "<a href=\"https://owasp.org/Top10/A03_2021-Injection/\">OWASP Top 10 2021 Category A3</a> - Injection </li>\n"));
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenHtmlDescriptionContainsNoHeaders_createOnlyRootCause() throws SQLException {
+ String noSection = "No sections";
+ insertSectionForLegacyHotspotRule(noSection);
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSections = findRuleDescriptionSections(LEGACY_HOTSPOT_RULE);
+
+ assertThat(ruleDescriptionSections)
+ .hasSize(1)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("root_cause", noSection));
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenLegacyIssueRule_doNotChangeSections() throws SQLException {
+ insertSectionForLegacyIssueRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSections = findRuleDescriptionSections(ISSUE_RULE);
+
+ assertThat(ruleDescriptionSections)
+ .hasSize(1)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("default", LEGACY_HOTSPOT_RULE_HTML_DESC));
+ }
+
+ @Test
+ public void fixHotspotRuleDescriptions_whenAdvancedRule_doNotChangeSections() throws SQLException {
+ insertSectionForAdvancedRule("root_cause", LEGACY_HOTSPOT_RULE_HTML_DESC);
+ insertSectionForAdvancedRule("assess_the_problem", LEGACY_HOTSPOT_RULE_HTML_DESC);
+ insertSectionForAdvancedRule("how_to_fix", LEGACY_HOTSPOT_RULE_HTML_DESC);
+ insertSectionForAdvancedRule("other", "blablabla");
+
+ fixHotspotRuleDescriptions.execute();
+
+ List<Map<String, Object>> ruleDescriptionSections = findRuleDescriptionSections(ADVANCED_RULE);
+
+ assertThat(ruleDescriptionSections)
+ .hasSize(4)
+ .extracting(r -> r.get("KEE"), r -> r.get("CONTENT"))
+ .contains(tuple("root_cause", LEGACY_HOTSPOT_RULE_HTML_DESC))
+ .contains(tuple("assess_the_problem", LEGACY_HOTSPOT_RULE_HTML_DESC))
+ .contains(tuple("how_to_fix", LEGACY_HOTSPOT_RULE_HTML_DESC))
+ .contains(tuple("other", "blablabla"));
+ }
+
+ @Test
+ public void insertRuleDescriptions_whenReentrant_doesNotFail() throws SQLException {
+ insertSectionForLegacyHotspotRule(LEGACY_HOTSPOT_RULE_HTML_DESC);
+
+ fixHotspotRuleDescriptions.execute();
+ fixHotspotRuleDescriptions.execute();
+ fixHotspotRuleDescriptions.execute();
+
+ assertThat(findRuleDescriptionSections(LEGACY_HOTSPOT_RULE)).hasSize(3);
+ }
+
+ private List<Map<String, Object>> findAllRuleDescriptionSections() {
+ return db.select("select uuid, kee, rule_uuid, content from "
+ + RULE_DESCRIPTION_SECTIONS_TABLE + "'");
+ }
+
+ private List<Map<String, Object>> findRuleDescriptionSections(String ruleUuid) {
+ return db.select("select uuid, kee, rule_uuid, content from "
+ + RULE_DESCRIPTION_SECTIONS_TABLE + " where rule_uuid = '" + ruleUuid + "'");
+ }
+
+ private void insertSectionForLegacyHotspotRule(String content) {
+ insertRuleDescriptionSection(LEGACY_HOTSPOT_RULE, DEFAULT_DESCRIPTION_KEY, content);
+ }
+
+ private void insertSectionForLegacyCustomHotspotRule(String content) {
+ insertRuleDescriptionSection(LEGACY_HOTSPOT_CUSTOM_RULE, DEFAULT_DESCRIPTION_KEY, content);
+ }
+
+ private void insertSectionForLegacyIssueRule(String content) {
+ insertRuleDescriptionSection(ISSUE_RULE, DEFAULT_DESCRIPTION_KEY, content);
+ }
+
+ private void insertSectionForAdvancedRule(String key, String content) {
+ insertRuleDescriptionSection(ADVANCED_RULE, key, content);
+ }
+
+ private void insertRuleDescriptionSection(String ruleUuid, String key, String content) {
+ Map<String, Object> ruleParams = new HashMap<>();
+ ruleParams.put("uuid", RandomStringUtils.randomAlphanumeric(20));
+ ruleParams.put("rule_uuid", ruleUuid);
+ ruleParams.put("kee", key);
+ ruleParams.put("content", content);
+
+ db.executeInsert(RULE_DESCRIPTION_SECTIONS_TABLE, ruleParams);
+ }
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest/schema.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest/schema.sql
new file mode 100644
index 00000000000..de8a0072e79
--- /dev/null
+++ b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v95/MigrateHotspotRuleDescriptionsTest/schema.sql
@@ -0,0 +1,35 @@
+CREATE TABLE "RULE_DESC_SECTIONS"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "RULE_UUID" CHARACTER VARYING(40) NOT NULL,
+ "KEE" CHARACTER VARYING(50) NOT NULL,
+ "CONTENT" CHARACTER LARGE OBJECT NOT NULL
+);
+ALTER TABLE "RULE_DESC_SECTIONS" ADD CONSTRAINT "PK_RULE_DESC_SECTIONS" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "UNIQ_RULE_DESC_SECTIONS_KEE" ON "RULE_DESC_SECTIONS"("RULE_UUID" NULLS FIRST, "KEE" NULLS FIRST);
+
+CREATE TABLE "RULES"(
+ "UUID" CHARACTER VARYING(40) NOT NULL,
+ "NAME" CHARACTER VARYING(200),
+ "PLUGIN_RULE_KEY" CHARACTER VARYING(200) NOT NULL,
+ "PLUGIN_KEY" CHARACTER VARYING(200),
+ "PLUGIN_CONFIG_KEY" CHARACTER VARYING(200),
+ "PLUGIN_NAME" CHARACTER VARYING(255) NOT NULL,
+ "SCOPE" CHARACTER VARYING(20) NOT NULL,
+ "PRIORITY" INTEGER,
+ "STATUS" CHARACTER VARYING(40),
+ "LANGUAGE" CHARACTER VARYING(20),
+ "DEF_REMEDIATION_FUNCTION" CHARACTER VARYING(20),
+ "DEF_REMEDIATION_GAP_MULT" CHARACTER VARYING(20),
+ "DEF_REMEDIATION_BASE_EFFORT" CHARACTER VARYING(20),
+ "GAP_DESCRIPTION" CHARACTER VARYING(4000),
+ "SYSTEM_TAGS" CHARACTER VARYING(4000),
+ "IS_TEMPLATE" BOOLEAN DEFAULT FALSE NOT NULL,
+ "DESCRIPTION_FORMAT" CHARACTER VARYING(20),
+ "RULE_TYPE" TINYINT,
+ "SECURITY_STANDARDS" CHARACTER VARYING(4000),
+ "IS_AD_HOC" BOOLEAN NOT NULL,
+ "IS_EXTERNAL" BOOLEAN NOT NULL,
+ "TEMPLATE_UUID" CHARACTER VARYING(40)
+);
+ALTER TABLE "RULES" ADD CONSTRAINT "PK_RULES" PRIMARY KEY("UUID");
+CREATE UNIQUE INDEX "RULES_REPO_KEY" ON "RULES"("PLUGIN_RULE_KEY" NULLS FIRST, "PLUGIN_NAME" NULLS FIRST);