]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16302 Adapt ExportRuleDto to handle new structure
authorZipeng WU <zipeng.wu@sonarsource.com>
Tue, 26 Apr 2022 08:49:06 +0000 (10:49 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 6 May 2022 20:02:43 +0000 (20:02 +0000)
20 files changed:
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/AdHocRuleCreatorTest.java
server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ExportRuleDto.java
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDescriptionSectionDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/qualityprofile/QualityProfileExportMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/rule/RuleMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/qualityprofile/QualityProfileExportDaoTest.java
server/sonar-db-dao/src/testFixtures/java/org/sonar/db/rule/RuleTesting.java
server/sonar-server-common/src/test/java/org/sonar/server/rule/HotspotRuleDescriptionTest.java
server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleIndexerTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ImportedRule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileBackuperImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileParser.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/NewCustomRule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/NewRuleDescriptionSection.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/RuleCreator.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/hotspot/ws/ShowActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileBackuperImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileParserTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/RuleCreatorTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ListActionTest.java

index d3e2524d08dc5560e67ff468f836f64bb62b771c..e9582b0ae186c01e9315e6f88cf04f461badf721 100644 (file)
@@ -104,7 +104,7 @@ public class AdHocRuleCreatorTest {
       .setEngineId("eslint")
       .setRuleId("no-cond-assign")
       .setName(repeat("a", 201))
-      .setDescription(repeat("a", 16_777_216))
+      .setDescription(repeat("a", 1_000_000))
       .setSeverity(Constants.Severity.BLOCKER)
       .setType(ScannerReport.IssueType.BUG)
       .build());
@@ -112,7 +112,7 @@ public class AdHocRuleCreatorTest {
     RuleDto rule = underTest.persistAndIndex(dbSession, addHocRule);
 
     assertThat(rule.getMetadata().getAdHocName()).isEqualTo(repeat("a", 200));
-    assertThat(rule.getMetadata().getAdHocDescription()).isEqualTo(repeat("a", 16_777_215));
+    assertThat(rule.getMetadata().getAdHocDescription()).isEqualTo(repeat("a", 1_000_000));
   }
 
   @Test
index fcd08f0cd989b2866d3cec184dbe99093dc1ccdb..3795c115eb70278c48a2144ceb3c6bf4d5c190cc 100644 (file)
@@ -21,23 +21,30 @@ package org.sonar.db.qualityprofile;
 
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.RuleType;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.SeverityUtil;
 
+import static java.util.Optional.ofNullable;
+import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
+
 public class ExportRuleDto {
   private String activeRuleUuid = null;
   private String repository = null;
   private String rule = null;
   private String name = null;
-  private String description = null;
   private String extendedDescription = null;
   private String template = null;
   private Integer severity = null;
   private Integer type = null;
   private String tags = null;
 
-  private List<ExportRuleParamDto> params;
+  private List<ExportRuleParamDto> params = null;
+
+  private Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = null;
 
   public boolean isCustomRule() {
     return template != null;
@@ -71,10 +78,6 @@ public class ExportRuleDto {
     return tags;
   }
 
-  public String getDescription() {
-    return description;
-  }
-
   public String getName() {
     return name;
   }
@@ -89,4 +92,17 @@ public class ExportRuleDto {
   void setParams(List<ExportRuleParamDto> params) {
     this.params = params;
   }
+
+  public Set<RuleDescriptionSectionDto> getRuleDescriptionSections() {
+    return ruleDescriptionSectionDtos;
+  }
+
+  public Optional<RuleDescriptionSectionDto> getDefaultRuleDescriptionSectionDto() {
+    return findExistingSectionWithSameKey(DEFAULT_KEY);
+  }
+
+  private Optional<RuleDescriptionSectionDto> findExistingSectionWithSameKey(String ruleDescriptionSectionKey) {
+    return ofNullable(ruleDescriptionSectionDtos).flatMap(sections ->
+      sections.stream().filter(section -> section.getKey().equals(ruleDescriptionSectionKey)).findAny());
+  }
 }
index 444f0e645223fb0b3fd378c276a481b6c005064a..208987e3f754df46517dd85513fce24b4c2714bf 100644 (file)
@@ -25,7 +25,7 @@ import java.util.StringJoiner;
 import static org.sonar.api.utils.Preconditions.checkArgument;
 
 public class RuleDescriptionSectionDto {
-  static final String DEFAULT_KEY = "default";
+  public static final String DEFAULT_KEY = "default";
 
   private final String uuid;
   private final String key;
index 5b1a7825a80bfdf23d1b3d969498e6b64e343a7c..4cea9e1e39b4ca8acd1cbec526046c8321a18608 100644 (file)
@@ -3,14 +3,25 @@
 
 <mapper namespace="org.sonar.db.qualityprofile.QualityProfileExportMapper">
 
+  <sql id="selectRuleDescriptionSectionColumns">
+    rds.uuid as "rds_uuid",
+    rds.kee as "rds_kee",
+    rds.description as "rds_description",
+  </sql>
+
+  <sql id="leftOuterJoinRulesDescriptionSections">
+    left outer join rule_desc_sections rds on
+    rds.rule_uuid = r.uuid
+  </sql>
+
   <sql id="exportRuleColumns">
+    <include refid="selectRuleDescriptionSectionColumns"/>
     a.uuid as "activeRuleUuid",
     a.failure_level as "severity",
     r.plugin_rule_key as "rule",
     r.plugin_name as "repository",
     r.priority as "defaultSeverity",
     r.name,
-    r.description,
     r.description_format as "descriptionFormat",
     r.rule_type as "type",
     rt.plugin_rule_key as "template",
     p.value as value
   </sql>
 
-  <select id="selectByProfileUuid" parameterType="string" resultType="org.sonar.db.qualityprofile.ExportRuleDto">
+  <resultMap id="ruleDefinitionResultMap" type="org.sonar.db.qualityprofile.ExportRuleDto">
+    <id property="activeRuleUuid" column="activeRuleUuid"/>
+    <result property="ruleKey" column="ruleKey"/>
+    <result property="severity" column="severity"/>
+    <result property="rule" column="rule"/>
+    <result property="repository" column="repository"/>
+    <result property="name" column="name"/>
+    <result property="type" column="type"/>
+    <result property="template" column="template"/>
+    <result property="extendedDescription" column="extendedDescription"/>
+    <result property="tags" column="tags"/>
+
+    <collection property="ruleDescriptionSectionDtos" ofType="org.sonar.db.rule.RuleDescriptionSectionDto">
+      <id property="uuid" column="rds_uuid"/>
+      <result property="key" column="rds_kee"/>
+      <result property="description" column="rds_description" typeHandler="org.apache.ibatis.type.StringTypeHandler"/>
+    </collection>
+
+  </resultMap>
+
+  <select id="selectByProfileUuid" parameterType="string" resultMap="ruleDefinitionResultMap">
     select
     <include refid="exportRuleColumns"/>
     from active_rules a
@@ -33,6 +64,7 @@
     inner join rules r on r.uuid = a.rule_uuid and r.status != 'REMOVED'
     left join rules rt on rt.uuid = r.template_uuid
     left join rules_metadata rm on rm.rule_uuid = r.uuid
+    <include refid="leftOuterJoinRulesDescriptionSections"/>
     where oqp.uuid = #{id, jdbcType=VARCHAR}
   </select>
 
index bb07883486ce91ab4a9e9631903254fe2402dc38..685b9be1a727429c6d153d9bd73da9f4f95d8952 100644 (file)
@@ -85,7 +85,7 @@
 
     <result property="createdAtFromDefinition" column="createdAtFromDefinition"/>
     <result property="updatedAtFromDefinition" column="updatedAtFromDefinition"/>
-    <result property="noteData" column="noteData"/>
+    <result property="noteData" column="noteData" typeHandler="org.apache.ibatis.type.StringTypeHandler"/>
     <result property="noteUserUuid" column="noteUserUuid"/>
     <result property="noteCreatedAt" column="noteCreatedAt"/>
     <result property="noteUpdatedAt" column="noteUpdatedAt"/>
@@ -94,7 +94,7 @@
     <result property="remediationBaseEffort" column="remediationBaseEffort"/>
     <result property="tagsField" column="tagsField"/>
     <result property="adHocName" column="adHocName"/>
-    <result property="adHocDescription" column="adHocDescription"/>
+    <result property="adHocDescription" column="adHocDescription" typeHandler="org.apache.ibatis.type.StringTypeHandler"/>
     <result property="adHocSeverity" column="adHocSeverity"/>
     <result property="adHocType" column="adHocType"/>
     <result property="createdAtFromMetadata" column="createdAtFromMetadata"/>
     <collection property="ruleDescriptionSectionDtos" ofType="org.sonar.db.rule.RuleDescriptionSectionDto">
       <id property="uuid" column="rds_uuid"/>
       <result property="key" column="rds_kee"/>
-      <result property="description" column="rds_description"/>
+      <result property="description" column="rds_description" typeHandler="org.apache.ibatis.type.StringTypeHandler"/>
     </collection>
   </resultMap>
 
     <collection property="ruleDescriptionSectionDtos" ofType="org.sonar.db.rule.RuleDescriptionSectionDto">
       <id property="uuid" column="rds_uuid"/>
       <result property="key" column="rds_kee"/>
-      <result property="description" column="rds_description"/>
+      <result property="description" column="rds_description" typeHandler="org.apache.ibatis.type.StringTypeHandler"/>
     </collection>
 
   </resultMap>
index fb9efff61ed3698b2abee1dddc1354afbf36e471..5e4912e4e640b8adb6466b2d7046230ecc99a463 100644 (file)
@@ -36,7 +36,6 @@ import org.sonar.api.rules.RuleType;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.rule.RuleDefinitionDto;
-import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleMetadataDto;
 import org.sonar.db.rule.RuleParamDto;
 
@@ -74,12 +73,14 @@ public class QualityProfileExportDaoTest {
     String language = "java";
     RuleDefinitionDto ruleTemplate = createRule(language);
     RuleDefinitionDto customRule = createRule(language, RuleStatus.READY, ruleTemplate.getUuid());
+    var customRuleDescription = customRule.getDefaultRuleDescriptionSectionDto().getDescription();
     RuleMetadataDto customRuleMetadata = createRuleMetadata(new RuleMetadataDto()
       .setRuleUuid(customRule.getUuid())
       .setNoteData("Extended description")
       .setTags(Sets.newHashSet("tag1", "tag2", "tag3")));
 
     RuleDefinitionDto rule = createRule(language, RuleStatus.READY, null);
+    var ruleDescription = rule.getDefaultRuleDescriptionSectionDto().getDescription();
     RuleMetadataDto ruleMetadata = createRuleMetadata(new RuleMetadataDto()
       .setRuleUuid(rule.getUuid()));
     QProfileDto profile = createProfile(language);
@@ -95,8 +96,7 @@ public class QualityProfileExportDaoTest {
     assertThat(exportCustomRuleDto).isNotNull();
     assertThat(exportCustomRuleDto.isCustomRule()).isTrue();
     assertThat(exportCustomRuleDto.getParams()).isEmpty();
-    //FIXME SONAR-16314
-    assertThat(exportCustomRuleDto.getDescription()).isEqualTo(customRule.getRuleDescriptionSectionDtos().stream().map(RuleDescriptionSectionDto::getDescription).collect(Collectors.joining()));
+    assertThat(exportCustomRuleDto.getRuleDescriptionSections().iterator().next().getDescription()).isEqualTo(customRuleDescription);
     assertThat(exportCustomRuleDto.getExtendedDescription()).isEqualTo(customRuleMetadata.getNoteData());
     assertThat(exportCustomRuleDto.getName()).isEqualTo(customRule.getName());
     assertThat(exportCustomRuleDto.getRuleKey()).isEqualTo(customRule.getKey());
@@ -112,8 +112,7 @@ public class QualityProfileExportDaoTest {
     assertThat(exportRuleDto).isNotNull();
     assertThat(exportRuleDto.isCustomRule()).isFalse();
     assertThat(exportRuleDto.getParams()).isEmpty();
-    //FIXME SONAR-16314
-    assertThat(exportRuleDto.getDescription()).isEqualTo(rule.getRuleDescriptionSectionDtos().stream().map(RuleDescriptionSectionDto::getDescription).collect(Collectors.joining()));
+    assertThat(exportRuleDto.getRuleDescriptionSections().iterator().next().getDescription()).isEqualTo(ruleDescription);
     assertThat(exportRuleDto.getExtendedDescription()).isEqualTo(ruleMetadata.getNoteData());
     assertThat(exportRuleDto.getName()).isEqualTo(rule.getName());
     assertThat(exportRuleDto.getRuleKey()).isEqualTo(rule.getKey());
index 28f8fc3d219741e536bbb650865f131d006cb22e..833f932a8b2dc1b16b9b4b244dc50bab65e7ca8a 100644 (file)
@@ -65,16 +65,16 @@ public class RuleTesting {
   }
 
   public static RuleDefinitionDto newRule(RuleKey key) {
-    RuleDefinitionDto ruleDefinitionDto = newRuleWithoutSection(key);
+    RuleDefinitionDto ruleDefinitionDto = newRuleWithoutDescriptionSection(key);
     ruleDefinitionDto.addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "description_" + randomAlphabetic(5)));
     return ruleDefinitionDto;
   }
 
-  public static RuleDefinitionDto newRuleWithoutSection() {
-    return newRuleWithoutSection(randomRuleKey());
+  public static RuleDefinitionDto newRuleWithoutDescriptionSection() {
+    return newRuleWithoutDescriptionSection(randomRuleKey());
   }
 
-  public static RuleDefinitionDto newRuleWithoutSection(RuleKey ruleKey) {
+  public static RuleDefinitionDto newRuleWithoutDescriptionSection(RuleKey ruleKey) {
     return new RuleDefinitionDto()
       .setRepositoryKey(ruleKey.repository())
       .setRuleKey(ruleKey.rule())
index 494b8af7a1664c8ebc01b87c91048943d80b4ef5..03983cb664cde40f85bf3b0866bc77efbb64612c 100644 (file)
@@ -26,18 +26,19 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleDto;
+import org.sonar.db.rule.RuleTesting;
 
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.db.rule.RuleTesting.newRuleWithoutSection;
+import static org.sonar.db.rule.RuleTesting.newRuleWithoutDescriptionSection;
 
 @RunWith(DataProviderRunner.class)
 public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_returns_all_empty_fields_when_no_description() {
-    RuleDefinitionDto dto = newRuleWithoutSection();
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection();
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
 
@@ -48,7 +49,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_returns_all_empty_fields_when_empty_description() {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", ""));
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", ""));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
 
@@ -60,7 +61,7 @@ public class HotspotRuleDescriptionTest {
   @Test
   @UseDataProvider("descriptionsWithoutTitles")
   public void parse_to_risk_description_fields_when_desc_contains_no_section(String description) {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", description));
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", description));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
 
@@ -81,7 +82,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_return_null_risk_when_desc_starts_with_ask_yourself_title() {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
       createDefaultRuleDescriptionSection("uuid", (ASKATRISK + RECOMMENTEDCODINGPRACTICE)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -93,7 +94,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() {
-    RuleDefinitionDto dto = newRuleWithoutSection()
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection()
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + RECOMMENTEDCODINGPRACTICE)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -105,7 +106,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() {
-    RuleDefinitionDto dto = newRuleWithoutSection()
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection()
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + ASKATRISK)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -117,7 +118,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_with_noncompliant_section_not_removed() {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
       createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + NONCOMPLIANTCODE + COMPLIANTCODE)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -129,7 +130,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_moved_noncompliant_code() {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
       createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + RECOMMENTEDCODINGPRACTICE + NONCOMPLIANTCODE + SEE)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -143,7 +144,7 @@ public class HotspotRuleDescriptionTest {
 
   @Test
   public void parse_moved_sensitivecode_code() {
-    RuleDefinitionDto dto = newRuleWithoutSection().addRuleDescriptionSectionDto(
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection().addRuleDescriptionSectionDto(
       createDefaultRuleDescriptionSection("uuid", (DESCRIPTION + ASKATRISK + RECOMMENTEDCODINGPRACTICE + SENSITIVECODE + SEE)));
 
     HotspotRuleDescription result = HotspotRuleDescription.from(dto);
@@ -160,7 +161,7 @@ public class HotspotRuleDescriptionTest {
     String askContent = "This is the ask section content";
     String recommendedContent = "This is the recommended section content";
 
-    RuleDefinitionDto dto = newRuleWithoutSection()
+    RuleDefinitionDto dto = RuleTesting.newRuleWithoutDescriptionSection()
       .setTemplateUuid("123")
       .setDescriptionFormat(RuleDto.Format.MARKDOWN)
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(
index 1c8a1ee73e18ae97b989369a76894c8f49539ce1..ff77b29101d3eaed5909a21a5885a0cc1bc5ef51 100644 (file)
@@ -44,6 +44,7 @@ import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.db.rule.RuleDescriptionSectionDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.rule.RuleDto.Scope;
+import org.sonar.db.rule.RuleTesting;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.security.SecurityStandards;
 import org.sonar.server.security.SecurityStandards.SQCategory;
@@ -56,7 +57,7 @@ import static java.util.stream.Collectors.toSet;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
-import static org.sonar.db.rule.RuleTesting.newRuleWithoutSection;
+import static org.sonar.db.rule.RuleTesting.newRuleWithoutDescriptionSection;
 import static org.sonar.server.rule.index.RuleIndexDefinition.TYPE_RULE;
 import static org.sonar.server.security.SecurityStandards.CWES_BY_SQ_CATEGORY;
 import static org.sonar.server.security.SecurityStandards.SQ_CATEGORY_KEYS_ORDERING;
@@ -147,7 +148,7 @@ public class RuleIndexerTest {
       .flatMap(t -> CWES_BY_SQ_CATEGORY.get(t).stream().map(e -> "cwe:" + e))
       .collect(toSet());
     SecurityStandards securityStandards = SecurityStandards.fromSecurityStandards(standards);
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT)
       .setSecurityStandards(standards)
       .addRuleDescriptionSectionDto(RULE_DESCRIPTION_SECTION_DTO));
@@ -182,7 +183,7 @@ public class RuleIndexerTest {
 
   @Test
   public void log_debug_when_hotspot_rule_no_description () {
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT));
     underTest.commitAndIndex(dbTester.getSession(), rule.getUuid());
 
@@ -195,7 +196,7 @@ public class RuleIndexerTest {
 
   @Test
   public void log_debug_when_hotspot_rule_description_has_none_of_the_key_titles() {
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT)
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), randomAlphabetic(30))));
     underTest.commitAndIndex(dbTester.getSession(), rule.getUuid());
@@ -209,7 +210,7 @@ public class RuleIndexerTest {
 
   @Test
   public void log_debug_when_hotspot_rule_description_is_missing_fixIt_tab_content() {
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT)
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "bar\n" +
         "<h2>Ask Yourself Whether</h2>\n" +
@@ -225,7 +226,7 @@ public class RuleIndexerTest {
 
   @Test
   public void log_debug_when_hotspot_rule_description_is_missing_risk_tab_content() {
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT)
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "<h2>Ask Yourself Whether</h2>\n" +
         "bar\n" +
@@ -242,7 +243,7 @@ public class RuleIndexerTest {
 
   @Test
   public void log_debug_when_hotspot_rule_description_is_missing_vulnerable_tab_content() {
-    RuleDefinitionDto rule = dbTester.rules().insert(newRuleWithoutSection()
+    RuleDefinitionDto rule = dbTester.rules().insert(RuleTesting.newRuleWithoutDescriptionSection()
       .setType(RuleType.SECURITY_HOTSPOT)
       .addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), "bar\n" +
         "<h2>Recommended Secure Coding Practices</h2>\n" +
index be99ea779a71ffab27a7c85809434add0c52920a..a9d2e60c01e3ff933731618f4134ab667f9244d9 100644 (file)
  */
 package org.sonar.server.qualityprofile;
 
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import org.sonar.api.rule.RuleKey;
+import org.sonar.server.rule.NewRuleDescriptionSection;
 
 class ImportedRule {
-  private RuleKey ruleKey = null;
-  private RuleKey templateKey = null;
+  private String key = null;
+
+  private String repository = null;
+
+  private String template = null;
   private String name = null;
   private String type = null;
   private String severity = null;
   private String description = null;
   private Map<String, String> parameters = null;
-
+  private Set<NewRuleDescriptionSection> ruleDescriptionSections = new HashSet<>();
   public Map<String, String> getParameters() {
     return parameters;
   }
 
   public RuleKey getRuleKey() {
-    return ruleKey;
+    return RuleKey.of(repository, key);
   }
 
   public RuleKey getTemplateKey() {
-    return templateKey;
+    return RuleKey.of(repository, template);
   }
 
   public String getName() {
@@ -59,16 +65,6 @@ class ImportedRule {
     return description;
   }
 
-  ImportedRule setRuleKey(RuleKey ruleKey) {
-    this.ruleKey = ruleKey;
-    return this;
-  }
-
-  ImportedRule setTemplateKey(RuleKey templateKey) {
-    this.templateKey = templateKey;
-    return this;
-  }
-
   ImportedRule setType(String type) {
     this.type = type;
     return this;
@@ -95,6 +91,26 @@ class ImportedRule {
   }
 
   boolean isCustomRule() {
-    return templateKey != null;
+    return template != null;
+  }
+
+  public Set<NewRuleDescriptionSection> getRuleDescriptionSections() {
+    return ruleDescriptionSections;
+  }
+
+  public void addRuleDescriptionSection(NewRuleDescriptionSection ruleDescriptionSection) {
+    this.ruleDescriptionSections.add(ruleDescriptionSection);
+  }
+
+  public void setRepository(String repository) {
+    this.repository = repository;
+  }
+
+  public void setTemplate(String template) {
+    this.template = template;
+  }
+
+  public void setKey(String key) {
+    this.key = key;
   }
 }
index 30dffad218e064c35d597e8bcd88053d76f86f34..de4bbb89cf6417808feaca7036e77b8b2f5a12bd 100644 (file)
@@ -45,10 +45,12 @@ import org.sonar.db.rule.DeprecatedRuleKeyDto;
 import org.sonar.db.rule.RuleDefinitionDto;
 import org.sonar.server.qualityprofile.builtin.QProfileName;
 import org.sonar.server.rule.NewCustomRule;
+import org.sonar.server.rule.NewRuleDescriptionSection;
 import org.sonar.server.rule.RuleCreator;
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toSet;
 
 @ServerSide
 public class QProfileBackuperImpl implements QProfileBackuper {
@@ -88,15 +90,20 @@ public class QProfileBackuperImpl implements QProfileBackuper {
     List<ImportedRule> importedRules = new ArrayList<>(exportRules.size());
 
     for (ExportRuleDto exportRuleDto : exportRules) {
+      var ruleKey = exportRuleDto.getRuleKey();
       ImportedRule importedRule = new ImportedRule();
       importedRule.setName(exportRuleDto.getName());
-      importedRule.setDescription(exportRuleDto.getDescription());
-      importedRule.setRuleKey(exportRuleDto.getRuleKey());
+      importedRule.setRepository(ruleKey.repository());
+      importedRule.setKey(ruleKey.rule());
       importedRule.setSeverity(exportRuleDto.getSeverityString());
       if (importedRule.isCustomRule()) {
-        importedRule.setTemplateKey(exportRuleDto.getTemplateRuleKey());
+        importedRule.setTemplate(exportRuleDto.getTemplateRuleKey().rule());
       }
       importedRule.setType(exportRuleDto.getRuleType().name());
+      exportRuleDto.getRuleDescriptionSections()
+        .stream()
+        .map(r -> new NewRuleDescriptionSection(r.getKey(), r.getDescription()))
+        .forEach(importedRule::addRuleDescriptionSection);
       importedRule.setParameters(exportRuleDto.getParams().stream().collect(Collectors.toMap(ExportRuleParamDto::getKey, ExportRuleParamDto::getValue)));
       importedRules.add(importedRule);
     }
@@ -154,13 +161,13 @@ public class QProfileBackuperImpl implements QProfileBackuper {
   private Map<RuleKey, RuleDefinitionDto> getImportedRulesDefinitions(DbSession dbSession, List<ImportedRule> rules) {
     Set<RuleKey> ruleKeys = rules.stream()
       .map(ImportedRule::getRuleKey)
-      .collect(Collectors.toSet());
+      .collect(toSet());
     Map<RuleKey, RuleDefinitionDto> rulesDefinitions = db.ruleDao().selectDefinitionByKeys(dbSession, ruleKeys).stream()
       .collect(Collectors.toMap(RuleDefinitionDto::getKey, identity()));
 
     Set<RuleKey> unrecognizedRuleKeys = ruleKeys.stream()
       .filter(r -> !rulesDefinitions.containsKey(r))
-      .collect(Collectors.toSet());
+      .collect(toSet());
 
     if (!unrecognizedRuleKeys.isEmpty()) {
       Map<String, DeprecatedRuleKeyDto> deprecatedRuleKeysByUuid = db.ruleDao().selectAllDeprecatedRuleKeys(dbSession).stream()
@@ -209,11 +216,12 @@ public class QProfileBackuperImpl implements QProfileBackuper {
   private static NewCustomRule importedRuleToNewCustomRule(ImportedRule r) {
     return NewCustomRule.createForCustomRule(r.getRuleKey().rule(), r.getTemplateKey())
       .setName(r.getName())
-      .setMarkdownDescription(r.getDescription())
       .setSeverity(r.getSeverity())
       .setStatus(RuleStatus.READY)
       .setPreventReactivation(true)
       .setType(RuleType.valueOf(r.getType()))
+      .setMarkdownDescription(r.getDescription())
+      .setRuleDescriptionSections(r.getRuleDescriptionSections())
       .setParameters(r.getParameters());
   }
 
index 7ea8fd88198d069e09afaac51ef0faab2d30c85a..f345d2fc4d49caaf3965a17a13efa9f23ba2df77 100644 (file)
@@ -42,15 +42,20 @@ import org.sonar.api.utils.text.XmlWriter;
 import org.sonar.db.qualityprofile.ExportRuleDto;
 import org.sonar.db.qualityprofile.ExportRuleParamDto;
 import org.sonar.db.qualityprofile.QProfileDto;
+import org.sonar.db.rule.RuleDescriptionSectionDto;
+import org.sonar.server.rule.NewRuleDescriptionSection;
 
 @ServerSide
 public class QProfileParser {
   private static final String ATTRIBUTE_PROFILE = "profile";
   private static final String ATTRIBUTE_NAME = "name";
   private static final String ATTRIBUTE_LANGUAGE = "language";
-
   private static final String ATTRIBUTE_RULES = "rules";
   private static final String ATTRIBUTE_RULE = "rule";
+  private static final String ATTRIBUTE_DESCRIPTION_SECTIONS = "descriptionSections";
+  private static final String ATTRIBUTE_DESCRIPTION_SECTION = "descriptionSection";
+  private static final String ATTRIBUTE_DESCRIPTION_SECTION_KEY = "key";
+  private static final String ATTRIBUTE_DESCRIPTION_SECTION_DESCRIPTION = "content";
   private static final String ATTRIBUTE_REPOSITORY_KEY = "repositoryKey";
   private static final String ATTRIBUTE_KEY = "key";
   private static final String ATTRIBUTE_PRIORITY = "priority";
@@ -80,7 +85,19 @@ public class QProfileParser {
       if (ruleToExport.isCustomRule()) {
         xml.prop(ATTRIBUTE_NAME, ruleToExport.getName());
         xml.prop(ATTRIBUTE_TEMPLATE_KEY, ruleToExport.getTemplateRuleKey().rule());
-        xml.prop(ATTRIBUTE_DESCRIPTION, ruleToExport.getDescription());
+        if (!ruleToExport.getRuleDescriptionSections().isEmpty()) {
+          ruleToExport.getDefaultRuleDescriptionSectionDto()
+            .map(RuleDescriptionSectionDto::getDescription)
+            .ifPresent(desc -> xml.prop(ATTRIBUTE_DESCRIPTION, desc));
+        }
+        xml.begin(ATTRIBUTE_DESCRIPTION_SECTIONS);
+        for (RuleDescriptionSectionDto ruleDescriptionSection : ruleToExport.getRuleDescriptionSections()) {
+          xml.begin(ATTRIBUTE_DESCRIPTION_SECTION)
+            .prop(ATTRIBUTE_DESCRIPTION_SECTION_KEY, ruleDescriptionSection.getKey())
+            .prop(ATTRIBUTE_DESCRIPTION_SECTION_DESCRIPTION, ruleDescriptionSection.getDescription())
+            .end();
+        }
+        xml.end(ATTRIBUTE_DESCRIPTION_SECTIONS);
       }
 
       xml.begin(ATTRIBUTE_PARAMETERS);
@@ -144,39 +161,10 @@ public class QProfileParser {
     while (rulesCursor.getNext() != null) {
       SMInputCursor ruleCursor = rulesCursor.childElementCursor();
       Map<String, String> parameters = new HashMap<>();
-      String repositoryKey = null;
-      String key = null;
-      String templateKey = null;
       ImportedRule rule = new ImportedRule();
-      while (ruleCursor.getNext() != null) {
-        String nodeName = ruleCursor.getLocalName();
-        if (StringUtils.equals(ATTRIBUTE_REPOSITORY_KEY, nodeName)) {
-          repositoryKey = StringUtils.trim(ruleCursor.collectDescendantText(false));
-        } else if (StringUtils.equals(ATTRIBUTE_KEY, nodeName)) {
-          key = StringUtils.trim(ruleCursor.collectDescendantText(false));
-        } else if (StringUtils.equals(ATTRIBUTE_TEMPLATE_KEY, nodeName)) {
-          templateKey = StringUtils.trim(ruleCursor.collectDescendantText(false));
-        } else if (StringUtils.equals(ATTRIBUTE_NAME, nodeName)) {
-          rule.setName(StringUtils.trim(ruleCursor.collectDescendantText(false)));
-        } else if (StringUtils.equals(ATTRIBUTE_TYPE, nodeName)) {
-          rule.setType(StringUtils.trim(ruleCursor.collectDescendantText(false)));
-        } else if (StringUtils.equals(ATTRIBUTE_DESCRIPTION, nodeName)) {
-          rule.setDescription(StringUtils.trim(ruleCursor.collectDescendantText(false)));
-        } else if (StringUtils.equals(ATTRIBUTE_PRIORITY, nodeName)) {
-          rule.setSeverity(StringUtils.trim(ruleCursor.collectDescendantText(false)));
-        } else if (StringUtils.equals(ATTRIBUTE_PARAMETERS, nodeName)) {
-          SMInputCursor propsCursor = ruleCursor.childElementCursor(ATTRIBUTE_PARAMETER);
-          readParameters(propsCursor, parameters);
-          rule.setParameters(parameters);
-        }
-      }
-      RuleKey ruleKey = RuleKey.of(repositoryKey, key);
-      rule.setRuleKey(ruleKey);
-
-      if (templateKey != null) {
-        rule.setTemplateKey(RuleKey.of(repositoryKey, templateKey));
-      }
+      readRule(ruleCursor, parameters, rule);
 
+      var ruleKey = rule.getRuleKey();
       if (activatedKeys.contains(ruleKey)) {
         duplicatedKeys.add(ruleKey);
       }
@@ -190,6 +178,34 @@ public class QProfileParser {
     return activations;
   }
 
+  private static void readRule(SMInputCursor ruleCursor, Map<String, String> parameters, ImportedRule rule) throws XMLStreamException {
+    while (ruleCursor.getNext() != null) {
+      String nodeName = ruleCursor.getLocalName();
+      if (StringUtils.equals(ATTRIBUTE_REPOSITORY_KEY, nodeName)) {
+        rule.setRepository(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_KEY, nodeName)) {
+        rule.setKey(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_TEMPLATE_KEY, nodeName)) {
+        rule.setTemplate(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_NAME, nodeName)) {
+        rule.setName(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_TYPE, nodeName)) {
+        rule.setType(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_DESCRIPTION, nodeName)) {
+        rule.setDescription(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_PRIORITY, nodeName)) {
+        rule.setSeverity(StringUtils.trim(ruleCursor.collectDescendantText(false)));
+      } else if (StringUtils.equals(ATTRIBUTE_PARAMETERS, nodeName)) {
+        SMInputCursor propsCursor = ruleCursor.childElementCursor(ATTRIBUTE_PARAMETER);
+        readParameters(propsCursor, parameters);
+        rule.setParameters(parameters);
+      } else if (StringUtils.equals(ATTRIBUTE_DESCRIPTION_SECTIONS, nodeName)) {
+        SMInputCursor propsCursor = ruleCursor.childElementCursor(ATTRIBUTE_DESCRIPTION_SECTION);
+        readDescriptionSections(propsCursor, rule);
+      }
+    }
+  }
+
   private static void readParameters(SMInputCursor propsCursor, Map<String, String> parameters) throws XMLStreamException {
     while (propsCursor.getNext() != null) {
       SMInputCursor propCursor = propsCursor.childElementCursor();
@@ -208,4 +224,23 @@ public class QProfileParser {
       }
     }
   }
+
+  private static void readDescriptionSections(SMInputCursor propsCursor, ImportedRule importedRule) throws XMLStreamException {
+    while (propsCursor.getNext() != null) {
+      SMInputCursor propCursor = propsCursor.childElementCursor();
+      String key = null;
+      String description = null;
+      while (propCursor.getNext() != null) {
+        String nodeName = propCursor.getLocalName();
+        if (StringUtils.equals(ATTRIBUTE_DESCRIPTION_SECTION_KEY, nodeName)) {
+          key = StringUtils.trim(propCursor.collectDescendantText(false));
+        } else if (StringUtils.equals(ATTRIBUTE_DESCRIPTION_SECTION_DESCRIPTION, nodeName)) {
+          description = StringUtils.trim(propCursor.collectDescendantText(false));
+        }
+      }
+      if (key != null && description != null) {
+        importedRule.addRuleDescriptionSection(new NewRuleDescriptionSection(key, description));
+      }
+    }
+  }
 }
index a803ede0bfc6567f44972ba3458ce45152a44345..d824eb927690570eb8286a63fcdc5e5c941f5281 100644 (file)
@@ -21,8 +21,11 @@ package org.sonar.server.rule;
 
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
+import java.util.Set;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.rule.RuleKey;
@@ -34,12 +37,13 @@ public class NewCustomRule {
   private String ruleKey;
   private RuleKey templateKey;
   private String name;
-  private String htmlDescription;
   private String markdownDescription;
   private String severity;
   private RuleStatus status;
   private RuleType type;
-  private final Map<String, String> parameters = new HashMap<>();
+  private Map<String, String> parameters = new HashMap<>();
+
+  private Set<NewRuleDescriptionSection> ruleDescriptionSections = new HashSet<>();
 
   private boolean preventReactivation = false;
 
@@ -66,16 +70,6 @@ public class NewCustomRule {
     return this;
   }
 
-  @CheckForNull
-  public String htmlDescription() {
-    return htmlDescription;
-  }
-
-  public NewCustomRule setHtmlDescription(@Nullable String htmlDescription) {
-    this.htmlDescription = htmlDescription;
-    return this;
-  }
-
   @CheckForNull
   public String markdownDescription() {
     return markdownDescription;
@@ -122,8 +116,16 @@ public class NewCustomRule {
   }
 
   public NewCustomRule setParameters(Map<String, String> params) {
-    this.parameters.clear();
-    this.parameters.putAll(params);
+    this.parameters = params;
+    return this;
+  }
+
+  public Set<NewRuleDescriptionSection> getRuleDescriptionSections() {
+    return Collections.unmodifiableSet(ruleDescriptionSections);
+  }
+
+  public NewCustomRule setRuleDescriptionSections(Set<NewRuleDescriptionSection> sections) {
+    this.ruleDescriptionSections = sections;
     return this;
   }
 
@@ -147,5 +149,4 @@ public class NewCustomRule {
     newRule.templateKey = templateKey;
     return newRule;
   }
-
 }
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/NewRuleDescriptionSection.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/NewRuleDescriptionSection.java
new file mode 100644 (file)
index 0000000..0812620
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.rule;
+
+public class NewRuleDescriptionSection {
+  private final String key;
+
+  private final String description;
+
+  public NewRuleDescriptionSection(String key, String description) {
+    this.key = key;
+    this.description = description;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
index b7d321877a789b0643a3a7907be69a3be25b937f..4b36e35c7f68c51d3fade5b5ea16cb962739a5c5 100644 (file)
@@ -168,11 +168,18 @@ public class RuleCreator {
   }
 
   private static void validateDescription(List<String> errors, NewCustomRule newRule) {
-    if (Strings.isNullOrEmpty(newRule.htmlDescription()) && Strings.isNullOrEmpty(newRule.markdownDescription())) {
+    boolean missingDescription = newRule.getRuleDescriptionSections().isEmpty() ?
+      Strings.isNullOrEmpty(newRule.markdownDescription()) :
+      noDescriptionSectionHasContent(newRule);
+    if (missingDescription) {
       errors.add("The description is missing");
     }
   }
 
+  private static boolean noDescriptionSectionHasContent(NewCustomRule newRule) {
+    return newRule.getRuleDescriptionSections().stream().map(NewRuleDescriptionSection::getDescription).allMatch(Strings::isNullOrEmpty);
+  }
+
   private static void validateRuleKey(List<String> errors, String ruleKey) {
     if (!ruleKey.matches("^[\\w]+$")) {
       errors.add(format("The rule key \"%s\" is invalid, it should only contain: a-z, 0-9, \"_\"", ruleKey));
@@ -207,10 +214,20 @@ public class RuleCreator {
       .setCreatedAt(system2.now())
       .setUpdatedAt(system2.now());
 
-    if (newRule.markdownDescription() != null) {
+    ruleDefinition.setDescriptionFormat(Format.MARKDOWN);
+
+    if (newRule.getRuleDescriptionSections().isEmpty() && newRule.markdownDescription() != null) {
       RuleDescriptionSectionDto ruleDescriptionSectionDto = createDefaultRuleDescriptionSection(uuidFactory.create(), newRule.markdownDescription());
-      ruleDefinition.setDescriptionFormat(Format.MARKDOWN);
       ruleDefinition.addRuleDescriptionSectionDto(ruleDescriptionSectionDto);
+    } else {
+      for (NewRuleDescriptionSection ruleDescriptionSection : newRule.getRuleDescriptionSections()) {
+        RuleDescriptionSectionDto ruleDescriptionSectionDto = RuleDescriptionSectionDto.builder()
+          .uuid(uuidFactory.create())
+          .key(ruleDescriptionSection.getKey())
+          .description(ruleDescriptionSection.getDescription())
+          .build();
+        ruleDefinition.addRuleDescriptionSectionDto(ruleDescriptionSectionDto);
+      }
     }
 
     dbClient.ruleDao().insert(dbSession, ruleDefinition);
index d791c487edc880839d0fea2a16a772d324e77fc4..ec3a28866ac8b78a34c364abcf57c93f25c84618 100644 (file)
@@ -1033,7 +1033,7 @@ public class ShowActionTest {
   }
 
   private RuleDefinitionDto newRuleWithoutSection(RuleType ruleType, Consumer<RuleDefinitionDto> populate) {
-    return newRule(ruleType, RuleTesting::newRuleWithoutSection, populate);
+    return newRule(ruleType, RuleTesting::newRuleWithoutDescriptionSection, populate);
   }
 
   private RuleDefinitionDto newRule(RuleType ruleType, Supplier<RuleDefinitionDto> ruleDefinitionDtoSupplier, Consumer<RuleDefinitionDto> populate) {
index 08e74098b12aed6a3c5290e2b2c33fdca6ccd8a1..542424a86616da129e9321a9d60a90338518a3ae 100644 (file)
@@ -56,6 +56,7 @@ import static org.mockito.ArgumentMatchers.anyList;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;
+import static org.sonar.db.rule.RuleTesting.newRuleWithoutDescriptionSection;
 
 public class QProfileBackuperImplTest {
 
@@ -166,6 +167,34 @@ public class QProfileBackuperImplTest {
       "<name>" + rule.getName() + "</name>" +
       "<templateKey>" + templateRule.getKey().rule() + "</templateKey>" +
       "<description>" + rule.getDefaultRuleDescriptionSectionDto().getDescription() + "</description>" +
+      "<descriptionSections><descriptionSection><key>default</key><content>" + rule.getDefaultRuleDescriptionSectionDto().getDescription() + "</content></descriptionSection></descriptionSections>" +
+      "<parameters><parameter>" +
+      "<key>" + param.getName() + "</key>" +
+      "<value>20</value>" +
+      "</parameter></parameters>" +
+      "</rule></rules></profile>");
+  }
+
+  @Test
+  public void backup_custom_rules_without_description_section() {
+    var rule = newRuleWithoutDescriptionSection();
+    db.rules().insert(rule);
+    RuleParamDto param = db.rules().insertRuleParam(rule);
+    QProfileDto profile = createProfile(rule.getLanguage());
+    ActiveRuleDto activeRule = activate(profile, rule, param);
+
+    StringWriter writer = new StringWriter();
+    underTest.backup(db.getSession(), profile, writer);
+
+    assertThat(writer).hasToString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
+      "<profile>" +
+      "<name>" + profile.getName() + "</name>" +
+      "<language>" + profile.getLanguage() + "</language>" +
+      "<rules><rule>" +
+      "<repositoryKey>" + rule.getRepositoryKey() + "</repositoryKey>" +
+      "<key>" + rule.getKey().rule() + "</key>" +
+      "<type>" + RuleType.valueOf(rule.getType()) + "</type>" +
+      "<priority>" + activeRule.getSeverityString() + "</priority>" +
       "<parameters><parameter>" +
       "<key>" + param.getName() + "</key>" +
       "<value>20</value>" +
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileParserTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/QProfileParserTest.java
new file mode 100644 (file)
index 0000000..aab0826
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * 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.qualityprofile;
+
+import java.io.Reader;
+import java.io.StringReader;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class QProfileParserTest {
+
+  @Test
+  public void readOlderVersionXml() {
+    Reader backup = new StringReader("<?xml version='1.0' encoding='UTF-8'?>" +
+      "<profile>" +
+      "<name>custom rule</name>" +
+      "<language>js</language>" +
+      "<rules><rule>" +
+      "<repositoryKey>sonarjs</repositoryKey>" +
+      "<key>s001</key>" +
+      "<type>CODE_SMELL</type>" +
+      "<priority>CRITICAL</priority>" +
+      "<name>custom rule name</name>" +
+      "<templateKey>rule_mc8</templateKey>" +
+      "<description>custom rule description</description>" +
+      "<parameters><parameter>" +
+      "<key>bar</key>" +
+      "<value>baz</value>" +
+      "</parameter>" +
+      "</parameters>" +
+      "</rule></rules></profile>");
+    var parser = new QProfileParser();
+    var importedQProfile = parser.readXml(backup);
+    assertThat(importedQProfile.getRules()).hasSize(1);
+    var importedRule = importedQProfile.getRules().get(0);
+    assertThat(importedRule.getDescription()).isEqualTo("custom rule description");
+  }
+
+  @Test
+  public void readNewVersionXml() {
+    Reader backup = new StringReader("<?xml version='1.0' encoding='UTF-8'?>" +
+      "<profile>" +
+      "<name>custom rule</name>" +
+      "<language>js</language>" +
+      "<rules><rule>" +
+      "<repositoryKey>sonarjs</repositoryKey>" +
+      "<key>s001</key>" +
+      "<type>CODE_SMELL</type>" +
+      "<priority>CRITICAL</priority>" +
+      "<name>custom rule name</name>" +
+      "<templateKey>rule_mc8</templateKey>" +
+      "<descriptionSections><descriptionSection><key>default</key><content>custom rule description</content></descriptionSection></descriptionSections>" +
+      "<parameters><parameter>" +
+      "<key>bar</key>" +
+      "<value>baz</value>" +
+      "</parameter>" +
+      "</parameters>" +
+      "</rule></rules></profile>");
+    var parser = new QProfileParser();
+    var importedQProfile = parser.readXml(backup);
+    assertThat(importedQProfile.getRules()).hasSize(1);
+    var importedRule = importedQProfile.getRules().get(0);
+    assertThat(importedRule.getRuleDescriptionSections()).hasSize(1);
+    var section = importedRule.getRuleDescriptionSections().iterator().next();
+    assertThat(section.getKey()).isEqualTo("default");
+    assertThat(section.getDescription()).isEqualTo("custom rule description");
+  }
+}
index 082fe1de06ac18544993513a0bd7df8f90d9c9ae..428f4d4ed044269ed86f3ab1ac0d233c7760d25d 100644 (file)
@@ -26,6 +26,7 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import org.assertj.core.api.Fail;
 import org.junit.Rule;
@@ -63,7 +64,6 @@ public class RuleCreatorTest {
 
   private System2 system2 = new TestSystem2().setNow(Instant.now().toEpochMilli());
 
-
   @Rule
   public DbTester dbTester = DbTester.create(system2);
 
@@ -125,13 +125,49 @@ public class RuleCreatorTest {
     assertThat(ruleIndex.search(new RuleQuery(), new SearchOptions()).getUuids()).containsOnly(rule.getUuid(), templateRule.getUuid());
   }
 
+  @Test
+  public void create_custom_rule_with_both_markdown_description_and_description_sections() {
+    // insert template rule
+    RuleDto templateRule = createTemplateRule();
+    // Create custom rule
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setName("My custom")
+      .setMarkdownDescription("Markdown description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "new description section")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY);
+    RuleKey customRuleKey = underTest.create(dbSession, newRule);
+
+    RuleDto rule = dbTester.getDbClient().ruleDao().selectOrFailByKey(dbSession, customRuleKey);
+    assertThat(rule).isNotNull();
+    assertThat(rule.getKey()).isEqualTo(RuleKey.of("java", "CUSTOM_RULE"));
+    assertThat(rule.getPluginKey()).isEqualTo("sonarjava");
+    assertThat(rule.getTemplateUuid()).isEqualTo(templateRule.getUuid());
+    assertThat(rule.getName()).isEqualTo("My custom");
+    assertThat(rule.getDefaultRuleDescriptionSection().getDescription()).isEqualTo("new description section");
+    assertThat(rule.getSeverityString()).isEqualTo("MAJOR");
+    assertThat(rule.getStatus()).isEqualTo(RuleStatus.READY);
+    assertThat(rule.getLanguage()).isEqualTo("java");
+    assertThat(rule.getConfigKey()).isEqualTo("S001");
+    assertThat(rule.getDefRemediationFunction()).isEqualTo("LINEAR_OFFSET");
+    assertThat(rule.getDefRemediationGapMultiplier()).isEqualTo("1h");
+    assertThat(rule.getDefRemediationBaseEffort()).isEqualTo("5min");
+    assertThat(rule.getGapDescription()).isEqualTo("desc");
+    assertThat(rule.getTags()).containsOnly("usertag1", "usertag2");
+    assertThat(rule.getSystemTags()).containsOnly("tag1", "tag4");
+    assertThat(rule.getSecurityStandards()).containsOnly("owaspTop10:a1", "cwe:123");
+    assertThat(rule.isExternal()).isFalse();
+    assertThat(rule.isAdHoc()).isFalse();
+  }
+
   @Test
   public void create_custom_rule_with_empty_parameter_value() {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
+    NewRuleDescriptionSection defaultSection = new NewRuleDescriptionSection("default", "some description");
     NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(defaultSection))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY)
       .setParameters(ImmutableMap.of("regex", ""));
@@ -153,7 +189,7 @@ public class RuleCreatorTest {
     RuleDefinitionDto templateRule = createTemplateRuleWithIntArrayParam();
     NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY);
 
@@ -174,7 +210,7 @@ public class RuleCreatorTest {
     RuleDefinitionDto templateRule = createTemplateRuleWithIntArrayParam();
     NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY)
       .setParameters(ImmutableMap.of("myIntegers", "1,3"));
@@ -197,13 +233,13 @@ public class RuleCreatorTest {
 
     NewCustomRule firstRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_1", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY);
 
     NewCustomRule secondRule = NewCustomRule.createForCustomRule("CUSTOM_RULE_2", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY);
 
@@ -224,16 +260,14 @@ public class RuleCreatorTest {
     dbTester.rules().insert(rule);
     dbSession.commit();
 
-    assertThatThrownBy(() -> {
-      // Create custom rule with unknown template rule
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, Collections.singletonList(newRule));
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, Collections.singletonList(newRule)))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("This rule is not a template rule: java:S001");
   }
@@ -338,7 +372,7 @@ public class RuleCreatorTest {
     // Create custom rule with same key, but with different values
     NewCustomRule newRule = NewCustomRule.createForCustomRule(key, templateRule.getKey())
       .setName("New name")
-      .setHtmlDescription("New description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY)
       .setParameters(ImmutableMap.of("regex", "c.*"))
@@ -359,15 +393,14 @@ public class RuleCreatorTest {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
 
-    assertThatThrownBy(() -> {
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("*INVALID*", templateRule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("*INVALID*", templateRule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(BadRequestException.class)
       .hasMessage("The rule key \"*INVALID*\" is invalid, it should only contain: a-z, 0-9, \"_\"");
   }
@@ -379,22 +412,21 @@ public class RuleCreatorTest {
     // Create a custom rule
     AtomicReference<NewCustomRule> newRule = new AtomicReference<>(NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
       .setName("My custom")
-      .setHtmlDescription("Some description")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
       .setSeverity(Severity.MAJOR)
       .setStatus(RuleStatus.READY)
       .setParameters(ImmutableMap.of("regex", "a.*")));
     underTest.create(dbSession, newRule.get());
 
-    assertThatThrownBy(() -> {
-      // Create another custom rule having same key
-      newRule.set(NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
-        .setName("My another custom")
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*")));
-      underTest.create(dbSession, newRule.get());
-    })
+    // Create another custom rule having same key
+    newRule.set(NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setName("My another custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*")));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule.get()))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("A rule with the key 'CUSTOM_RULE' already exists");
   }
@@ -404,14 +436,13 @@ public class RuleCreatorTest {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
 
-    assertThatThrownBy(() -> {
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(BadRequestException.class)
       .hasMessage("The name is missing");
   }
@@ -438,14 +469,13 @@ public class RuleCreatorTest {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
 
-    assertThatThrownBy(() -> {
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(BadRequestException.class)
       .hasMessage("The severity is missing");
   }
@@ -455,15 +485,14 @@ public class RuleCreatorTest {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
 
-    assertThatThrownBy(() -> {
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setSeverity("INVALID")
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity("INVALID")
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(BadRequestException.class)
       .hasMessage("Severity \"INVALID\" is invalid");
   }
@@ -473,14 +502,13 @@ public class RuleCreatorTest {
     // insert template rule
     RuleDto templateRule = createTemplateRule();
 
-    assertThatThrownBy(() -> {
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", templateRule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(BadRequestException.class)
       .hasMessage("The status is missing");
   }
@@ -492,16 +520,15 @@ public class RuleCreatorTest {
     dbTester.rules().insert(rule);
     dbSession.commit();
 
-    assertThatThrownBy(() -> {
-      // Create custom rule with unknown template rule
-      NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
-        .setName("My custom")
-        .setHtmlDescription("Some description")
-        .setSeverity(Severity.MAJOR)
-        .setStatus(RuleStatus.READY)
-        .setParameters(ImmutableMap.of("regex", "a.*"));
-      underTest.create(dbSession, newRule);
-    })
+    // Create custom rule with unknown template rule
+    NewCustomRule newRule = NewCustomRule.createForCustomRule("CUSTOM_RULE", rule.getKey())
+      .setName("My custom")
+      .setRuleDescriptionSections(Set.of(new NewRuleDescriptionSection("default", "some description")))
+      .setSeverity(Severity.MAJOR)
+      .setStatus(RuleStatus.READY)
+      .setParameters(ImmutableMap.of("regex", "a.*"));
+
+    assertThatThrownBy(() -> underTest.create(dbSession, newRule))
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("This rule is not a template rule: java:S001");
   }
index 9a17510240d8e9a38348bae9a829f4a5316bcaee..3e6c8eee400d009db6abe7a170eea63477a1de6d 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.rule.ws;
 
+import java.util.Optional;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.rule.RuleKey;
@@ -33,6 +34,9 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 public class ListActionTest {
 
+  private static final String RULE_KEY_1 = "S001";
+  private static final String RULE_KEY_2 = "S002";
+
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
 
@@ -48,8 +52,8 @@ public class ListActionTest {
 
   @Test
   public void return_rules_in_protobuf() {
-    dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", "S001")).setConfigKey(null).setName(null));
-    dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", "S002")).setConfigKey("I002").setName("Rule Two"));
+    dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_1)).setConfigKey(null).setName(null));
+    dbTester.rules().insert(RuleTesting.newRule(RuleKey.of("java", RULE_KEY_2)).setConfigKey("I002").setName("Rule Two"));
     dbTester.getSession().commit();
 
     Rules.ListResponse listResponse = tester.newRequest()
@@ -57,11 +61,22 @@ public class ListActionTest {
 
     assertThat(listResponse.getRulesCount()).isEqualTo(2);
 
-    assertThat(listResponse.getRules(0).getKey()).isEqualTo("S001");
-    assertThat(listResponse.getRules(0).getInternalKey()).isEmpty();
-    assertThat(listResponse.getRules(0).getName()).isEmpty();
-    assertThat(listResponse.getRules(1).getKey()).isEqualTo("S002");
-    assertThat(listResponse.getRules(1).getInternalKey()).isEqualTo("I002");
-    assertThat(listResponse.getRules(1).getName()).isEqualTo("Rule Two");
+    Rules.ListResponse.Rule ruleS001 = getRule(listResponse, RULE_KEY_1);
+    assertThat(ruleS001.getKey()).isEqualTo(RULE_KEY_1);
+    assertThat(ruleS001.getInternalKey()).isEmpty();
+    assertThat(ruleS001.getName()).isEmpty();
+
+    Rules.ListResponse.Rule ruleS002 = getRule(listResponse, RULE_KEY_2);
+    assertThat(ruleS002.getKey()).isEqualTo(RULE_KEY_2);
+    assertThat(ruleS002.getInternalKey()).isEqualTo("I002");
+    assertThat(ruleS002.getName()).isEqualTo("Rule Two");
+  }
+
+  private Rules.ListResponse.Rule getRule(Rules.ListResponse listResponse, String ruleKey) {
+    Optional<Rules.ListResponse.Rule> rule = listResponse.getRulesList().stream()
+      .filter(r -> ruleKey.equals(r.getKey()))
+      .findFirst();
+    assertThat(rule).isPresent();
+    return rule.get();
   }
 }