From: Aurelien Poscia Date: Mon, 30 May 2022 10:05:19 +0000 (+0200) Subject: SONAR-16397 Add DB migration to migrate rule descriptions to new structure X-Git-Tag: 9.5.0.56709~75 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=cdeca9bf6ccd0cb1a3b2d397efa8ace1ef4789c2;p=sonarqube.git SONAR-16397 Add DB migration to migrate rule descriptions to new structure --- 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 selectRuleDescriptionSection = findExistingRuleDescriptions(context); + if (selectRuleDescriptionSection.isEmpty()) { + return; + } + insertRuleDescSections(context, selectRuleDescriptionSection); + } + + private static List 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 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 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 generateNewNamedSections(String descriptionInHtml) { + String[] split = extractSection("", descriptionInHtml); + String remainingText = split[0]; + String ruleDescriptionSection = split[1]; + + split = extractSection("

Exceptions

", remainingText); + remainingText = split[0]; + String exceptions = split[1]; + + split = extractSection("

Ask Yourself Whether

", remainingText); + remainingText = split[0]; + String askSection = split[1]; + + split = extractSection("

Sensitive Code Example

", remainingText); + remainingText = split[0]; + String sensitiveSection = split[1]; + + split = extractSection("

Noncompliant Code Example

", remainingText); + remainingText = split[0]; + String noncompliantSection = split[1]; + + split = extractSection("

Recommended Secure Coding Practices

", remainingText); + remainingText = split[0]; + String recommendedSection = split[1]; + + split = extractSection("

Compliant Solution

", remainingText); + remainingText = split[0]; + String compliantSection = split[1]; + + split = extractSection("

See

", remainingText); + remainingText = split[0]; + String seeSection = split[1]; + + Map 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 sections) throws SQLException { + for (Map.Entry 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 = "

"; + 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 result1 = findRuleSectionDescription(uuid1); + private void assertRuleDescriptionCreated(String uuid, String description1) { + Map 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 = "

Formatted SQL queries can be difficult to maintain" + + "

Ask Yourself Whether

\n" + + "balbalblabla\n" + + "

Recommended Secure Coding Practices

\n" + + "Consider using ORM frameworks" + + "

Sensitive Code Example

\n" + + "mysql\n" + + "

Compliant Solution

\n" + + "compliant solution desc\n" + + "

Exceptions

\n" + + "

This rule current implementation does not follow variables.

\n" + + "

See

\n" + + "OWASP Top 10 2021 Category A3 - Injection \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 ... additionalKeyValues) { + Map 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> ruleDescriptionSections = findRuleDescriptionSections(LEGACY_HOTSPOT_RULE); + + assertThat(ruleDescriptionSections).hasSize(3) + .extracting(r -> r.get("KEE"), r -> r.get("CONTENT")) + .contains(tuple("root_cause", "

Formatted SQL queries can be difficult to maintain

Exceptions

\n" + + "

This rule current implementation does not follow variables.

\n")) + .contains(tuple("assess_the_problem", "

Ask Yourself Whether

\n" + + "balbalblabla\n" + + "

Sensitive Code Example

\n" + + "mysql\n")) + .contains(tuple("how_to_fix", "

Recommended Secure Coding Practices

\n" + + "Consider using ORM frameworks

Compliant Solution

\n" + + "compliant solution desc\n" + + "

See

\n" + + "OWASP Top 10 2021 Category A3 - Injection \n")); + } + + @Test + public void fixHotspotRuleDescriptions_whenCustomRule_doNotCreateSections() throws SQLException { + insertSectionForLegacyCustomHotspotRule(LEGACY_HOTSPOT_RULE_HTML_DESC); + + fixHotspotRuleDescriptions.execute(); + + List> 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> ruleDescriptionSectionsLegacyHotspotRule = findRuleDescriptionSections(LEGACY_HOTSPOT_RULE); + List> ruleDescriptionSectionsIssueRule = findRuleDescriptionSections(ISSUE_RULE); + List> 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", "

Formatted SQL queries can be difficult to maintain

Exceptions

\n" + + "

This rule current implementation does not follow variables.

\n")) + .contains(tuple("assess_the_problem", "

Ask Yourself Whether

\n" + + "balbalblabla\n" + + "

Sensitive Code Example

\n" + + "mysql\n")) + .contains(tuple("how_to_fix", "

Recommended Secure Coding Practices

\n" + + "Consider using ORM frameworks

Compliant Solution

\n" + + "compliant solution desc\n" + + "

See

\n" + + "OWASP Top 10 2021 Category A3 - Injection \n")); + } + + @Test + public void fixHotspotRuleDescriptions_whenHtmlDescriptionContainsNoHeaders_createOnlyRootCause() throws SQLException { + String noSection = "No sections"; + insertSectionForLegacyHotspotRule(noSection); + + fixHotspotRuleDescriptions.execute(); + + List> 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> 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> 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> findAllRuleDescriptionSections() { + return db.select("select uuid, kee, rule_uuid, content from " + + RULE_DESCRIPTION_SECTIONS_TABLE + "'"); + } + + private List> 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 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);