Browse Source

SONAR-16397 Store rule description sections in the new DB structure and split when necessary

tags/9.5.0.56709
Aurelien Poscia 2 years ago
parent
commit
fcb80c0ade
14 changed files with 1100 additions and 34 deletions
  1. 7
    5
      server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java
  2. 53
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java
  3. 146
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java
  4. 60
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java
  5. 41
    28
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java
  6. 32
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java
  7. 44
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java
  8. 101
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java
  9. 239
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java
  10. 14
    1
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java
  11. 135
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java
  12. 76
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java
  13. 144
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java
  14. 8
    0
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java

+ 7
- 5
server/sonar-db-dao/src/main/java/org/sonar/db/rule/RuleDto.java View File

@@ -21,6 +21,7 @@ package org.sonar.db.rule;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
@@ -179,16 +180,17 @@ public class RuleDto {
return ruleDescriptionSectionDtos;
}

@CheckForNull
public RuleDescriptionSectionDto getRuleDescriptionSectionDto(String ruleDescriptionSectionKey) {
return findExistingSectionWithSameKey(ruleDescriptionSectionKey).orElse(null);
}

@CheckForNull
public RuleDescriptionSectionDto getDefaultRuleDescriptionSection() {
return findExistingSectionWithSameKey(DEFAULT_KEY).orElse(null);
}

public RuleDto replaceRuleDescriptionSectionDtos(Collection<RuleDescriptionSectionDto> ruleDescriptionSectionDtos) {
this.ruleDescriptionSectionDtos.clear();
ruleDescriptionSectionDtos.forEach(this::addRuleDescriptionSectionDto);
return this;
}

public RuleDto addRuleDescriptionSectionDto(RuleDescriptionSectionDto ruleDescriptionSectionDto) {
checkArgument(sectionWithSameKeyShouldNotExist(ruleDescriptionSectionDto),
"A section with key %s already exists", ruleDescriptionSectionDto.getKey());

+ 53
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGenerator.java View File

@@ -0,0 +1,53 @@
/*
* 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;

import java.util.Set;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;

import static java.util.stream.Collectors.toSet;

public class AdvancedRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
private final UuidFactory uuidFactory;

public AdvancedRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
this.uuidFactory = uuidFactory;
}

@Override
public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
return !rule.ruleDescriptionSections().isEmpty();
}

@Override
public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
return rule.ruleDescriptionSections().stream()
.map(section -> RuleDescriptionSectionDto.builder()
.uuid(uuidFactory.create())
.key(section.getKey())
.content(section.getHtmlContent())
.build()
)
.collect(toSet());
}

}

+ 146
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGenerator.java View File

@@ -0,0 +1,146 @@
/*
* 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;

import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.CheckForNull;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;
import org.sonar.markdown.Markdown;

import static java.util.Collections.emptySet;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
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;

public class LegacyHotspotRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
private final UuidFactory uuidFactory;

public LegacyHotspotRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
this.uuidFactory = uuidFactory;
}

@Override
public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
return SECURITY_HOTSPOT.equals(rule.type()) && rule.ruleDescriptionSections().isEmpty();
}

@Override
public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
return getDescriptionInHtml(rule)
.map(this::generateSections)
.orElse(emptySet());
}

private static Optional<String> getDescriptionInHtml(RulesDefinition.Rule rule) {
if (rule.htmlDescription() != null) {
return Optional.of(rule.htmlDescription());
} else if (rule.markdownDescription() != null) {
return Optional.of(Markdown.convertToHtml(rule.markdownDescription()));
}
return Optional.empty();
}

private Set<RuleDescriptionSectionDto> generateSections(String descriptionInHtml) {
String[] split = extractSection("", descriptionInHtml);
String remainingText = split[0];
String ruleDescriptionSection = split[1];

split = extractSection("<h2>Exceptions</h2>", remainingText);
remainingText = split[0];
String exceptions = split[1];

split = extractSection("<h2>Ask Yourself Whether</h2>", remainingText);
remainingText = split[0];
String askSection = split[1];

split = extractSection("<h2>Sensitive Code Example</h2>", remainingText);
remainingText = split[0];
String sensitiveSection = split[1];

split = extractSection("<h2>Noncompliant Code Example</h2>", remainingText);
remainingText = split[0];
String noncompliantSection = split[1];

split = extractSection("<h2>Recommended Secure Coding Practices</h2>", remainingText);
remainingText = split[0];
String recommendedSection = split[1];

split = extractSection("<h2>Compliant Solution</h2>", remainingText);
remainingText = split[0];
String compliantSection = split[1];

split = extractSection("<h2>See</h2>", remainingText);
remainingText = split[0];
String seeSection = split[1];

RuleDescriptionSectionDto rootSection = createSection(ROOT_CAUSE_SECTION_KEY, ruleDescriptionSection, exceptions, remainingText);
RuleDescriptionSectionDto assessSection = createSection(ASSESS_THE_PROBLEM_SECTION_KEY, askSection, sensitiveSection, noncompliantSection);
RuleDescriptionSectionDto fixSection = createSection(HOW_TO_FIX_SECTION_KEY, recommendedSection, compliantSection, seeSection);

return Stream.of(rootSection, assessSection, fixSection)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
}


private static String[] extractSection(String beginning, String description) {
String endSection = "<h2>";
int beginningIndex = description.indexOf(beginning);
if (beginningIndex != -1) {
int endIndex = description.indexOf(endSection, beginningIndex + beginning.length());
if (endIndex == -1) {
endIndex = description.length();
}
return new String[] {
description.substring(0, beginningIndex) + description.substring(endIndex),
description.substring(beginningIndex, endIndex)
};
} else {
return new String[] {description, ""};
}

}

@CheckForNull
private RuleDescriptionSectionDto createSection(String key, String... contentPieces) {
String content = trimToNull(String.join("", contentPieces));
if (content == null) {
return null;
}
return RuleDescriptionSectionDto.builder()
.uuid(uuidFactory.create())
.key(key)
.content(content)
.build();
}

@CheckForNull
private static String trimToNull(String input) {
return input.isEmpty() ? null : input;
}

}

+ 60
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/LegacyIssueRuleDescriptionSectionsGenerator.java View File

@@ -0,0 +1,60 @@
/*
* 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;

import java.util.EnumSet;
import java.util.Set;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static org.apache.commons.lang.StringUtils.isNotEmpty;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.rules.RuleType.VULNERABILITY;
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;

public class LegacyIssueRuleDescriptionSectionsGenerator implements RuleDescriptionSectionsGenerator {
private static final Set<RuleType> ISSUE_RULE_TYPES = EnumSet.of(CODE_SMELL, BUG, VULNERABILITY);

private final UuidFactory uuidFactory;

public LegacyIssueRuleDescriptionSectionsGenerator(UuidFactory uuidFactory) {
this.uuidFactory = uuidFactory;
}

@Override
public boolean isGeneratorForRule(RulesDefinition.Rule rule) {
return ISSUE_RULE_TYPES.contains(rule.type()) && rule.ruleDescriptionSections().isEmpty();
}

@Override
public Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule) {
if (isNotEmpty(rule.htmlDescription())) {
return singleton(createDefaultRuleDescriptionSection(uuidFactory.create(), rule.htmlDescription()));
} else if (isNotEmpty(rule.markdownDescription())) {
return singleton(createDefaultRuleDescriptionSection(uuidFactory.create(), rule.markdownDescription()));
}
return emptySet();
}
}

+ 41
- 28
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RegisterRules.java View File

@@ -81,7 +81,6 @@ import static org.apache.commons.lang.StringUtils.isNotEmpty;
import static org.sonar.core.util.stream.MoreCollectors.toList;
import static org.sonar.core.util.stream.MoreCollectors.toSet;
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;

/**
* Register rules at server startup
@@ -100,10 +99,13 @@ public class RegisterRules implements Startable {
private final WebServerRuleFinder webServerRuleFinder;
private final UuidFactory uuidFactory;
private final MetadataIndex metadataIndex;
private final RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver;


public RegisterRules(RuleDefinitionsLoader defLoader, QProfileRules qProfileRules, DbClient dbClient, RuleIndexer ruleIndexer,
ActiveRuleIndexer activeRuleIndexer, Languages languages, System2 system2,
WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex) {
WebServerRuleFinder webServerRuleFinder, UuidFactory uuidFactory, MetadataIndex metadataIndex,
RuleDescriptionSectionsGeneratorResolver ruleDescriptionSectionsGeneratorResolver) {
this.defLoader = defLoader;
this.qProfileRules = qProfileRules;
this.dbClient = dbClient;
@@ -114,6 +116,7 @@ public class RegisterRules implements Startable {
this.webServerRuleFinder = webServerRuleFinder;
this.uuidFactory = uuidFactory;
this.metadataIndex = metadataIndex;
this.ruleDescriptionSectionsGeneratorResolver = ruleDescriptionSectionsGeneratorResolver;
}

@Override
@@ -394,18 +397,16 @@ public class RegisterRules implements Startable {
.setIsAdHoc(false)
.setCreatedAt(system2.now())
.setUpdatedAt(system2.now());
String htmlDescription = ruleDef.htmlDescription();
if (isNotEmpty(htmlDescription)) {
ruleDto.addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), htmlDescription));

if (isNotEmpty(ruleDef.htmlDescription())) {
ruleDto.setDescriptionFormat(Format.HTML);
} else {
String markdownDescription = ruleDef.markdownDescription();
if (isNotEmpty(markdownDescription)) {
ruleDto.addRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), markdownDescription));
ruleDto.setDescriptionFormat(Format.MARKDOWN);
}
} else if (isNotEmpty(ruleDef.markdownDescription())) {
ruleDto.setDescriptionFormat(Format.MARKDOWN);
}

generateRuleDescriptionSections(ruleDef)
.forEach(ruleDto::addRuleDescriptionSectionDto);

DebtRemediationFunction debtRemediationFunction = ruleDef.debtRemediationFunction();
if (debtRemediationFunction != null) {
ruleDto.setDefRemediationFunction(debtRemediationFunction.type().name());
@@ -418,6 +419,11 @@ public class RegisterRules implements Startable {
return ruleDto;
}

private Set<RuleDescriptionSectionDto> generateRuleDescriptionSections(RulesDefinition.Rule ruleDef) {
RuleDescriptionSectionsGenerator descriptionSectionGenerator = ruleDescriptionSectionsGeneratorResolver.getRuleDescriptionSectionsGenerator(ruleDef);
return descriptionSectionGenerator.generateSections(ruleDef);
}

private static Scope toDtoScope(RuleScope scope) {
switch (scope) {
case ALL:
@@ -491,28 +497,35 @@ public class RegisterRules implements Startable {
}

private boolean mergeDescription(RulesDefinition.Rule rule, RuleDto ruleDto) {
boolean changed = false;

String currentDescription = Optional.ofNullable(ruleDto.getDefaultRuleDescriptionSection())
.map(RuleDescriptionSectionDto::getContent)
.orElse(null);

String htmlDescription = rule.htmlDescription();
String markdownDescription = rule.markdownDescription();
if (isDescriptionUpdated(htmlDescription, currentDescription)) {
ruleDto.addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), htmlDescription));
Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos = generateRuleDescriptionSections(rule);
if (ruleDescriptionSectionsUnchanged(ruleDto, newRuleDescriptionSectionDtos)) {
return false;
}
ruleDto.replaceRuleDescriptionSectionDtos(newRuleDescriptionSectionDtos);
if (containsHtmlDescription(rule)) {
ruleDto.setDescriptionFormat(Format.HTML);
changed = true;
} else if (isDescriptionUpdated(markdownDescription, currentDescription)) {
ruleDto.addOrReplaceRuleDescriptionSectionDto(createDefaultRuleDescriptionSection(uuidFactory.create(), markdownDescription));
return true;
} else if (isNotEmpty(rule.markdownDescription())) {
ruleDto.setDescriptionFormat(Format.MARKDOWN);
changed = true;
return true;
}
return changed;
return false;
}

private static boolean containsHtmlDescription(RulesDefinition.Rule rule) {
return isNotEmpty(rule.htmlDescription()) || !rule.ruleDescriptionSections().isEmpty();
}

private static boolean ruleDescriptionSectionsUnchanged(RuleDto ruleDto, Set<RuleDescriptionSectionDto> newRuleDescriptionSectionDtos) {
Map<String, String> oldKeysToSections = toMap(ruleDto.getRuleDescriptionSectionDtos());
Map<String, String> newKeysToSections = toMap(newRuleDescriptionSectionDtos);
return oldKeysToSections.equals(newKeysToSections);
}

private static boolean isDescriptionUpdated(@Nullable String description, @Nullable String currentDescription) {
return isNotEmpty(description) && !Objects.equals(description, currentDescription);
private static Map<String, String> toMap(Set<RuleDescriptionSectionDto> ruleDto) {
return ruleDto
.stream()
.collect(Collectors.toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
}

private static boolean mergeDebtDefinitions(RulesDefinition.Rule def, RuleDto dto) {

+ 32
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGenerator.java View File

@@ -0,0 +1,32 @@
/*
* 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;

import java.util.Set;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.db.rule.RuleDescriptionSectionDto;

public interface RuleDescriptionSectionsGenerator {

boolean isGeneratorForRule(RulesDefinition.Rule rule);

Set<RuleDescriptionSectionDto> generateSections(RulesDefinition.Rule rule);

}

+ 44
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolver.java View File

@@ -0,0 +1,44 @@
/*
* 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;

import java.util.Set;
import org.sonar.api.server.rule.RulesDefinition;

import static java.util.stream.Collectors.toSet;
import static org.sonar.api.utils.Preconditions.checkState;

public class RuleDescriptionSectionsGeneratorResolver {
private final Set<RuleDescriptionSectionsGenerator> ruleDescriptionSectionsGenerators;

RuleDescriptionSectionsGeneratorResolver(Set<RuleDescriptionSectionsGenerator> ruleDescriptionSectionsGenerators) {
this.ruleDescriptionSectionsGenerators = ruleDescriptionSectionsGenerators;
}

RuleDescriptionSectionsGenerator getRuleDescriptionSectionsGenerator(RulesDefinition.Rule ruleDef) {
Set<RuleDescriptionSectionsGenerator> generatorsFound = ruleDescriptionSectionsGenerators.stream()
.filter(generator -> generator.isGeneratorForRule(ruleDef))
.collect(toSet());
checkState(generatorsFound.size() < 2, "More than one rule description section generator found for rule with key %s", ruleDef.key());
checkState(!generatorsFound.isEmpty(), "No rule description section generator found for rule with key %s", ruleDef.key());
return generatorsFound.iterator().next();
}

}

+ 101
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/AdvancedRuleDescriptionSectionsGeneratorTest.java View File

@@ -0,0 +1,101 @@
/*
* 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;

import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.server.rule.RuleDescriptionSection;
import org.sonar.api.server.rule.RuleDescriptionSectionBuilder;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;

import static java.util.Collections.emptyList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
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;

@RunWith(MockitoJUnitRunner.class)
public class AdvancedRuleDescriptionSectionsGeneratorTest {
private static final String UUID_1 = "uuid1";
private static final String UUID_2 = "uuid2";

private static final String HTML_CONTENT = "html content";

private static final RuleDescriptionSection SECTION_1 = new RuleDescriptionSectionBuilder().sectionKey(HOW_TO_FIX_SECTION_KEY).htmlContent(HTML_CONTENT).build();
private static final RuleDescriptionSection SECTION_2 = new RuleDescriptionSectionBuilder().sectionKey(ROOT_CAUSE_SECTION_KEY).htmlContent(HTML_CONTENT + "2").build();

private static final RuleDescriptionSectionDto EXPECTED_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(HOW_TO_FIX_SECTION_KEY).content(HTML_CONTENT).build();
private static final RuleDescriptionSectionDto EXPECTED_SECTION_2 = RuleDescriptionSectionDto.builder().uuid(UUID_2).key(ROOT_CAUSE_SECTION_KEY)
.content(HTML_CONTENT + "2").build();

@Mock
private UuidFactory uuidFactory;

@Mock
private RulesDefinition.Rule rule;

@InjectMocks
private AdvancedRuleDescriptionSectionsGenerator generator;

@Before
public void before() {
when(uuidFactory.create()).thenReturn(UUID_1).thenReturn(UUID_2);
}

@Test
public void generateSections_whenOneSection_createsOneSections() {
when(rule.ruleDescriptionSections()).thenReturn(List.of(SECTION_1));

Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);

assertThat(ruleDescriptionSectionDtos)
.usingRecursiveFieldByFieldElementComparator()
.containsOnly(EXPECTED_SECTION_1);
}

@Test
public void generateSections_whenTwoSections_createsTwoSections() {
when(rule.ruleDescriptionSections()).thenReturn(List.of(SECTION_1, SECTION_2));

Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);

assertThat(ruleDescriptionSectionDtos)
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrder(EXPECTED_SECTION_1, EXPECTED_SECTION_2);
}

@Test
public void generateSections_whenNoSections_returnsEmptySet() {
when(rule.ruleDescriptionSections()).thenReturn(emptyList());

Set<RuleDescriptionSectionDto> ruleDescriptionSectionDtos = generator.generateSections(rule);

assertThat(ruleDescriptionSectionDtos).isEmpty();
}

}

+ 239
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/LegacyHotspotRuleDescriptionSectionsGeneratorTest.java View File

@@ -0,0 +1,239 @@
/*
* 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;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;

import static java.util.stream.Collectors.toMap;
import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
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;

@RunWith(DataProviderRunner.class)
public class LegacyHotspotRuleDescriptionSectionsGeneratorTest {

/*
* Bunch of static constant to create rule description.
*/
private static final String DESCRIPTION =
"<p>The use of operators pairs ( <code>=+</code>, <code>=-</code> or <code>=!</code> ) where the reversed, single operator was meant (<code>+=</code>,\n"
+ "<code>-=</code> or <code>!=</code>) will compile and run, but not produce the expected results.</p>\n"
+ "<p>This rule raises an issue when <code>=+</code>, <code>=-</code>, or <code>=!</code> is used without any spacing between the two operators and when\n"
+ "there is at least one whitespace character after.</p>\n";
private static final String NONCOMPLIANTCODE = "<h2>Noncompliant Code Example</h2>\n" + "<pre>Integer target = -5;\n" + "Integer num = 3;\n" + "\n"
+ "target =- num; // Noncompliant; target = -3. Is that really what's meant?\n" + "target =+ num; // Noncompliant; target = 3\n" + "</pre>\n";

private static final String COMPLIANTCODE =
"<h2>Compliant Solution</h2>\n" + "<pre>Integer target = -5;\n" + "Integer num = 3;\n" + "\n" + "target = -num; // Compliant; intent to assign inverse value of num is clear\n"
+ "target += num;\n" + "</pre>\n";

private static final String SEE =
"<h2>See</h2>\n" + "<ul>\n" + " <li> <a href=\"https://cwe.mitre.org/data/definitions/352.html\">MITRE, CWE-352</a> - Cross-Site Request Forgery (CSRF) </li>\n"
+ " <li> <a href=\"https://www.owasp.org/index.php/Top_10-2017_A6-Security_Misconfiguration\">OWASP Top 10 2017 Category A6</a> - Security\n" + " Misconfiguration </li>\n"
+ " <li> <a href=\"https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29\">OWASP: Cross-Site Request Forgery</a> </li>\n"
+ " <li> <a href=\"https://www.sans.org/top25-software-errors/#cat1\">SANS Top 25</a> - Insecure Interaction Between Components </li>\n"
+ " <li> Derived from FindSecBugs rule <a href=\"https://find-sec-bugs.github.io/bugs.htm#SPRING_CSRF_PROTECTION_DISABLED\">SPRING_CSRF_PROTECTION_DISABLED</a> </li>\n"
+ " <li> <a href=\"https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#when-to-use-csrf-protection\">Spring Security\n"
+ " Official Documentation: When to use CSRF protection</a> </li>\n" + "</ul>\n";

private static final String RECOMMENTEDCODINGPRACTICE =
"<h2>Recommended Secure Coding Practices</h2>\n" + "<ul>\n" + " <li> activate Spring Security's CSRF protection. </li>\n" + "</ul>\n";

private static final String ASKATRISK =
"<h2>Ask Yourself Whether</h2>\n" + "<ul>\n" + " <li> Any URLs responding with <code>Access-Control-Allow-Origin: *</code> include sensitive content. </li>\n"
+ " <li> Any domains specified in <code>Access-Control-Allow-Origin</code> headers are checked against a whitelist. </li>\n" + "</ul>\n";

private static final String SENSITIVECODE = "<h2>Sensitive Code Example</h2>\n" + "<pre>\n" + "// === Java Servlet ===\n" + "@Override\n"
+ "protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {\n"
+ " resp.setHeader(\"Content-Type\", \"text/plain; charset=utf-8\");\n" + " resp.setHeader(\"Access-Control-Allow-Origin\", \"http://localhost:8080\"); // Questionable\n"
+ " resp.setHeader(\"Access-Control-Allow-Credentials\", \"true\"); // Questionable\n" + " resp.setHeader(\"Access-Control-Allow-Methods\", \"GET\"); // Questionable\n"
+ " resp.getWriter().write(\"response\");\n" + "}\n" + "</pre>\n" + "<pre>\n" + "// === Spring MVC Controller annotation ===\n"
+ "@CrossOrigin(origins = \"http://domain1.com\") // Questionable\n" + "@RequestMapping(\"\")\n" + "public class TestController {\n"
+ " public String home(ModelMap model) {\n" + " model.addAttribute(\"message\", \"ok \");\n" + " return \"view\";\n" + " }\n" + "\n"
+ " @CrossOrigin(origins = \"http://domain2.com\") // Questionable\n" + " @RequestMapping(value = \"/test1\")\n" + " public ResponseEntity&lt;String&gt; test1() {\n"
+ " return ResponseEntity.ok().body(\"ok\");\n" + " }\n" + "}\n" + "</pre>\n";

private final UuidFactory uuidFactory = mock(UuidFactory.class);
private final RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);

private final LegacyHotspotRuleDescriptionSectionsGenerator generator = new LegacyHotspotRuleDescriptionSectionsGenerator(uuidFactory);

@Before
public void setUp() {
when(rule.htmlDescription()).thenReturn(null);
when(rule.markdownDescription()).thenReturn(null);
}

@Test
public void parse_returns_all_empty_fields_when_no_description() {
when(rule.htmlDescription()).thenReturn(null);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

assertThat(results).isEmpty();
}

@Test
public void parse_returns_all_empty_fields_when_empty_description() {
when(rule.htmlDescription()).thenReturn("");

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

assertThat(results).isEmpty();
}

@Test
@UseDataProvider("descriptionsWithoutTitles")
public void parse_to_risk_description_fields_when_desc_contains_no_section(String description) {
when(rule.htmlDescription()).thenReturn(description);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

assertThat(results).hasSize(1);
RuleDescriptionSectionDto uniqueSection = results.iterator().next();
assertThat(uniqueSection.getKey()).isEqualTo(ROOT_CAUSE_SECTION_KEY);
assertThat(uniqueSection.getContent()).isEqualTo(description);
}

@DataProvider
public static Object[][] descriptionsWithoutTitles() {
return new Object[][] {{randomAlphabetic(123)}, {"bar\n" + "acme\n" + "foo"}};
}

@Test
public void parse_return_null_risk_when_desc_starts_with_ask_yourself_title() {
when(rule.htmlDescription()).thenReturn(ASKATRISK + RECOMMENTEDCODINGPRACTICE);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(2)
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK)
.containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE);
}


@Test
public void parse_return_null_vulnerable_when_no_ask_yourself_whether_title() {
when(rule.htmlDescription()).thenReturn(DESCRIPTION + RECOMMENTEDCODINGPRACTICE);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(2)
.containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
.containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE);
}

@Test
public void parse_return_null_fixIt_when_desc_has_no_Recommended_Secure_Coding_Practices_title() {
when(rule.htmlDescription()).thenReturn(DESCRIPTION + ASKATRISK);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(2)
.containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK);
}

@Test
public void parse_with_noncompliant_section_not_removed() {
when(rule.htmlDescription()).thenReturn(DESCRIPTION + NONCOMPLIANTCODE + COMPLIANTCODE);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(3)
.containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NONCOMPLIANTCODE)
.containsEntry(HOW_TO_FIX_SECTION_KEY, COMPLIANTCODE);
}

@Test
public void parse_moved_noncompliant_code() {
when(rule.htmlDescription()).thenReturn(DESCRIPTION + RECOMMENTEDCODINGPRACTICE + NONCOMPLIANTCODE + SEE);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(3)
.containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, NONCOMPLIANTCODE)
.containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE + SEE);
}

@Test
public void parse_moved_sensitivecode_code() {
when(rule.htmlDescription()).thenReturn(DESCRIPTION + ASKATRISK + RECOMMENTEDCODINGPRACTICE + SENSITIVECODE + SEE);

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(3)
.containsEntry(ROOT_CAUSE_SECTION_KEY, DESCRIPTION)
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY, ASKATRISK + SENSITIVECODE)
.containsEntry(HOW_TO_FIX_SECTION_KEY, RECOMMENTEDCODINGPRACTICE + SEE);
}

@Test
public void parse_md_rule_description() {
String ruleDescription = "This is the custom rule description";
String exceptionsContent = "This the exceptions section content";
String askContent = "This is the ask section content";
String recommendedContent = "This is the recommended section content";

when(rule.markdownDescription()).thenReturn(ruleDescription + "\n"
+ "== Exceptions" + "\n"
+ exceptionsContent + "\n"
+ "== Ask Yourself Whether" + "\n"
+ askContent + "\n"
+ "== Recommended Secure Coding Practices" + "\n"
+ recommendedContent + "\n");

Set<RuleDescriptionSectionDto> results = generator.generateSections(rule);

Map<String, String> sectionKeyToContent = results.stream().collect(toMap(RuleDescriptionSectionDto::getKey, RuleDescriptionSectionDto::getContent));
assertThat(sectionKeyToContent).hasSize(3)
.containsEntry(ROOT_CAUSE_SECTION_KEY, ruleDescription + "<br/>"
+ "<h2>Exceptions</h2>"
+ exceptionsContent + "<br/>")
.containsEntry(ASSESS_THE_PROBLEM_SECTION_KEY,"<h2>Ask Yourself Whether</h2>"
+ askContent + "<br/>")
.containsEntry(HOW_TO_FIX_SECTION_KEY, "<h2>Recommended Secure Coding Practices</h2>"
+ recommendedContent + "<br/>");

}

}

+ 14
- 1
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RegisterRulesTest.java View File

@@ -89,6 +89,7 @@ import static org.sonar.api.server.rule.RulesDefinition.NewRule;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10;
import static org.sonar.api.server.rule.RulesDefinition.OwaspTop10Version.Y2021;
import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY;
import static org.sonar.db.rule.RuleDescriptionSectionDto.builder;
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection;

@RunWith(DataProviderRunner.class)
@@ -125,12 +126,23 @@ public class RegisterRulesTest {
private RuleIndexer ruleIndexer;
private ActiveRuleIndexer activeRuleIndexer;
private RuleIndex ruleIndex;
private RuleDescriptionSectionsGenerator ruleDescriptionSectionsGenerator = mock(RuleDescriptionSectionsGenerator.class);
private RuleDescriptionSectionsGeneratorResolver resolver = mock(RuleDescriptionSectionsGeneratorResolver.class);

@Before
public void before() {
ruleIndexer = new RuleIndexer(es.client(), dbClient);
ruleIndex = new RuleIndex(es.client(), system);
activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client());
when(resolver.getRuleDescriptionSectionsGenerator(any())).thenReturn(ruleDescriptionSectionsGenerator);
when(ruleDescriptionSectionsGenerator.generateSections(any())).thenAnswer(answer -> {
RulesDefinition.Rule rule = answer.getArgument(0, RulesDefinition.Rule.class);
String description = rule.htmlDescription() == null ? rule.markdownDescription() : rule.htmlDescription();
return Set.of(builder().uuid(UuidFactoryFast.getInstance().create()).key("default").content(description).build());
});


when(ruleDescriptionSectionsGenerator.isGeneratorForRule(any())).thenReturn(true);
}

@Test
@@ -989,7 +1001,8 @@ public class RegisterRulesTest {
when(languages.get(any())).thenReturn(mock(Language.class));
reset(webServerRuleFinder);

RegisterRules task = new RegisterRules(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex);
RegisterRules task = new RegisterRules(loader, qProfileRules, dbClient, ruleIndexer, activeRuleIndexer, languages, system, webServerRuleFinder, uuidFactory, metadataIndex,
resolver);
task.start();
// Execute a commit to refresh session state as the task is using its own session
db.getSession().commit();

+ 135
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionGeneratorTestData.java View File

@@ -0,0 +1,135 @@
/*
* 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;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import javax.annotation.Nullable;
import org.sonar.api.rules.RuleType;
import org.sonar.api.server.rule.RuleDescriptionSection;
import org.sonar.db.rule.RuleDescriptionSectionDto;

class RuleDescriptionGeneratorTestData {
enum RuleDescriptionSectionGeneratorIdentifier {
LEGACY_ISSUE, LEGACY_HOTSPOT, ADVANCED_RULE;
}

private final RuleType ruleType;
private final String htmlDescription;

private final String markdownDescription;
private final List<RuleDescriptionSection> ruleDescriptionSections;
private final RuleDescriptionSectionGeneratorIdentifier expectedGenerator;
private final Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto;

private RuleDescriptionGeneratorTestData(RuleType ruleType, @Nullable String htmlDescription,@Nullable String markdownDescription, List<RuleDescriptionSection> ruleDescriptionSections,
RuleDescriptionSectionGeneratorIdentifier expectedGenerator, Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto) {
this.ruleType = ruleType;
this.htmlDescription = htmlDescription;
this.markdownDescription = markdownDescription;
this.ruleDescriptionSections = ruleDescriptionSections;
this.expectedGenerator = expectedGenerator;
this.expectedRuleDescriptionSectionsDto = expectedRuleDescriptionSectionsDto;
}

public RuleType getRuleType() {
return ruleType;
}

String getHtmlDescription() {
return htmlDescription;
}

String getMarkdownDescription() {
return markdownDescription;
}

List<RuleDescriptionSection> getRuleDescriptionSections() {
return ruleDescriptionSections;
}

RuleDescriptionSectionGeneratorIdentifier getExpectedGenerator() {
return expectedGenerator;
}

public Set<RuleDescriptionSectionDto> getExpectedRuleDescriptionSectionsDto() {
return expectedRuleDescriptionSectionsDto;
}

static RuleDescriptionGeneratorTestDataBuilder aRuleOfType(RuleType ruleType) {
return new RuleDescriptionGeneratorTestDataBuilder(ruleType);
}

@Override
public String toString() {
return new StringJoiner(", ")
.add(ruleType.name())
.add(htmlDescription == null ? "html present" : "html absent")
.add(markdownDescription == null ? "md present" : "md absent")
.add(String.valueOf(ruleDescriptionSections.size()))
.add("generator=" + expectedGenerator)
.toString();
}

public static final class RuleDescriptionGeneratorTestDataBuilder {
private final RuleType ruleType;
private String htmlDescription;
private String markdownDescription;
private List<RuleDescriptionSection> ruleDescriptionSections = new ArrayList<>();
private Set<RuleDescriptionSectionDto> expectedRuleDescriptionSectionsDto = new HashSet<>();
private RuleDescriptionSectionGeneratorIdentifier expectedGenerator;

private RuleDescriptionGeneratorTestDataBuilder(RuleType ruleType) {
this.ruleType = ruleType;
}

RuleDescriptionGeneratorTestDataBuilder html(@Nullable String htmlDescription) {
this.htmlDescription = htmlDescription;
return this;
}

RuleDescriptionGeneratorTestDataBuilder md(@Nullable String markdownDescription) {
this.markdownDescription = markdownDescription;
return this;
}

RuleDescriptionGeneratorTestDataBuilder addSection(RuleDescriptionSection ruleDescriptionSection) {
this.ruleDescriptionSections.add(ruleDescriptionSection);
return this;
}

RuleDescriptionGeneratorTestDataBuilder expectedGenerator(RuleDescriptionSectionGeneratorIdentifier generatorToUse) {
this.expectedGenerator = generatorToUse;
return this;
}

RuleDescriptionGeneratorTestDataBuilder addExpectedSection(RuleDescriptionSectionDto sectionDto) {
this.expectedRuleDescriptionSectionsDto.add(sectionDto);
return this;
}

RuleDescriptionGeneratorTestData build() {
return new RuleDescriptionGeneratorTestData(ruleType, htmlDescription, markdownDescription, ruleDescriptionSections, expectedGenerator, expectedRuleDescriptionSectionsDto);
}
}
}

+ 76
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorResolverTest.java View File

@@ -0,0 +1,76 @@
/*
* 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;

import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.server.rule.RulesDefinition;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class RuleDescriptionSectionsGeneratorResolverTest {

private static final String RULE_KEY = "RULE_KEY";

@Mock
private RuleDescriptionSectionsGenerator generator1;
@Mock
private RuleDescriptionSectionsGenerator generator2;
@Mock
private RulesDefinition.Rule rule;

private RuleDescriptionSectionsGeneratorResolver resolver;

@Before
public void setUp() {
resolver = new RuleDescriptionSectionsGeneratorResolver(Set.of(generator1, generator2));
when(rule.key()).thenReturn(RULE_KEY);
}

@Test
public void getRuleDescriptionSectionsGenerator_returnsTheCorrectGenerator() {
when(generator2.isGeneratorForRule(rule)).thenReturn(true);
assertThat(resolver.getRuleDescriptionSectionsGenerator(rule)).isEqualTo(generator2);
}

@Test
public void getRuleDescriptionSectionsGenerator_whenNoGeneratorFound_throwsWithCorrectMessage() {
assertThatIllegalStateException()
.isThrownBy(() -> resolver.getRuleDescriptionSectionsGenerator(rule))
.withMessage("No rule description section generator found for rule with key RULE_KEY");
}

@Test
public void getRuleDescriptionSectionsGenerator_whenMoreThanOneGeneratorFound_throwsWithCorrectMessage() {
when(generator1.isGeneratorForRule(rule)).thenReturn(true);
when(generator2.isGeneratorForRule(rule)).thenReturn(true);
assertThatIllegalStateException()
.isThrownBy(() -> resolver.getRuleDescriptionSectionsGenerator(rule))
.withMessage("More than one rule description section generator found for rule with key RULE_KEY");
}

}

+ 144
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/rule/RuleDescriptionSectionsGeneratorsTest.java View File

@@ -0,0 +1,144 @@
/*
* 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;

import com.google.common.collect.ImmutableMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.sonar.api.server.rule.RuleDescriptionSection;
import org.sonar.api.server.rule.RuleDescriptionSectionBuilder;
import org.sonar.api.server.rule.RulesDefinition;
import org.sonar.core.util.UuidFactory;
import org.sonar.db.rule.RuleDescriptionSectionDto;
import org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.api.rules.RuleType.BUG;
import static org.sonar.api.rules.RuleType.CODE_SMELL;
import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT;
import static org.sonar.api.rules.RuleType.VULNERABILITY;
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY;
import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.ADVANCED_RULE;
import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.LEGACY_HOTSPOT;
import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.RuleDescriptionSectionGeneratorIdentifier.LEGACY_ISSUE;
import static org.sonar.server.rule.RuleDescriptionGeneratorTestData.aRuleOfType;

@RunWith(Parameterized.class)
public class RuleDescriptionSectionsGeneratorsTest {

private static final String KEY_1 = "KEY";
private static final String KEY_2 = "KEY_2";
private static final String UUID_1 = "uuid1";
private static final String UUID_2 = "uuid2";

private static final String HTML_CONTENT = "html content";
private static final String MD_CONTENT = "md content balblab";

private static final RuleDescriptionSection SECTION_1 = new RuleDescriptionSectionBuilder().sectionKey(KEY_1).htmlContent(HTML_CONTENT).build();
private static final RuleDescriptionSection SECTION_2 = new RuleDescriptionSectionBuilder().sectionKey(KEY_2).htmlContent(HTML_CONTENT).build();

private static final RuleDescriptionSectionDto DEFAULT_HTML_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key("default").content(HTML_CONTENT).build();
private static final RuleDescriptionSectionDto DEFAULT_HTML_HOTSPOT_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(ROOT_CAUSE_SECTION_KEY).content(HTML_CONTENT).build();
private static final RuleDescriptionSectionDto DEFAULT_MD_HOTSPOT_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(ROOT_CAUSE_SECTION_KEY).content(MD_CONTENT).build();
private static final RuleDescriptionSectionDto DEFAULT_MD_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key("default").content(MD_CONTENT).build();
private static final RuleDescriptionSectionDto HTML_SECTION_1 = RuleDescriptionSectionDto.builder().uuid(UUID_1).key(KEY_1).content(HTML_CONTENT).build();
private static final RuleDescriptionSectionDto HTML_SECTION_2 = RuleDescriptionSectionDto.builder().uuid(UUID_2).key(KEY_2).content(HTML_CONTENT).build();

@Parameterized.Parameters(name = "{index} = {0}")
public static List<RuleDescriptionGeneratorTestData> testData() {
return Arrays.asList(
// ISSUES
aRuleOfType(BUG).html(null).md(null).expectedGenerator(LEGACY_ISSUE).build(),
aRuleOfType(BUG).html(HTML_CONTENT).md(null).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
aRuleOfType(BUG).html(null).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_MD_SECTION_1).build(),
aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
aRuleOfType(CODE_SMELL).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
aRuleOfType(VULNERABILITY).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_ISSUE).addExpectedSection(DEFAULT_HTML_SECTION_1).build(),
// HOTSPOT
aRuleOfType(SECURITY_HOTSPOT).html(null).md(null).expectedGenerator(LEGACY_HOTSPOT).build(),
aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(null).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_HTML_HOTSPOT_SECTION_1).build(),
aRuleOfType(SECURITY_HOTSPOT).html(null).md(MD_CONTENT).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_MD_HOTSPOT_SECTION_1).build(),
aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(MD_CONTENT).expectedGenerator(LEGACY_HOTSPOT).addExpectedSection(DEFAULT_HTML_HOTSPOT_SECTION_1).build(),
// ADVANCED RULES
aRuleOfType(BUG).html(null).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(BUG).html(HTML_CONTENT).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(BUG).html(null).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(BUG).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).addSection(SECTION_2).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1)
.addExpectedSection(
HTML_SECTION_2).build(),
aRuleOfType(SECURITY_HOTSPOT).html(null).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(null).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(SECURITY_HOTSPOT).html(null).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build(),
aRuleOfType(SECURITY_HOTSPOT).html(HTML_CONTENT).md(MD_CONTENT).addSection(SECTION_1).expectedGenerator(ADVANCED_RULE).addExpectedSection(HTML_SECTION_1).build()
);
}

private final UuidFactory uuidFactory = mock(UuidFactory.class);
private final RulesDefinition.Rule rule = mock(RulesDefinition.Rule.class);

private final RuleDescriptionGeneratorTestData testData;

private final RuleDescriptionSectionsGenerator advancedRuleDescriptionSectionsGenerator = new AdvancedRuleDescriptionSectionsGenerator(uuidFactory);
private final RuleDescriptionSectionsGenerator legacyHotspotRuleDescriptionSectionsGenerator = new LegacyHotspotRuleDescriptionSectionsGenerator(uuidFactory);
private final RuleDescriptionSectionsGenerator legacyIssueRuleDescriptionSectionsGenerator = new LegacyIssueRuleDescriptionSectionsGenerator(uuidFactory);

Map<RuleDescriptionSectionGeneratorIdentifier, RuleDescriptionSectionsGenerator> idToGenerator = ImmutableMap.<RuleDescriptionSectionGeneratorIdentifier, RuleDescriptionSectionsGenerator>builder()
.put(ADVANCED_RULE, advancedRuleDescriptionSectionsGenerator)
.put(LEGACY_HOTSPOT, legacyHotspotRuleDescriptionSectionsGenerator)
.put(LEGACY_ISSUE, legacyIssueRuleDescriptionSectionsGenerator)
.build();

public RuleDescriptionSectionsGeneratorsTest(RuleDescriptionGeneratorTestData testData) {
this.testData = testData;
}

@Before
public void before() {
when(uuidFactory.create()).thenReturn(UUID_1).thenReturn(UUID_2);
when(rule.htmlDescription()).thenReturn(testData.getHtmlDescription());
when(rule.markdownDescription()).thenReturn(testData.getMarkdownDescription());
when(rule.ruleDescriptionSections()).thenReturn(testData.getRuleDescriptionSections());
when(rule.type()).thenReturn(testData.getRuleType());
}

@Test
public void scenario() {
assertThat(advancedRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(ADVANCED_RULE.equals(testData.getExpectedGenerator()));
assertThat(legacyHotspotRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(LEGACY_HOTSPOT.equals(testData.getExpectedGenerator()));
assertThat(legacyIssueRuleDescriptionSectionsGenerator.isGeneratorForRule(rule)).isEqualTo(LEGACY_ISSUE.equals(testData.getExpectedGenerator()));

generateAndVerifySectionsContent(idToGenerator.get(testData.getExpectedGenerator()));
}

private void generateAndVerifySectionsContent(RuleDescriptionSectionsGenerator advancedRuleDescriptionSectionsGenerator) {
assertThat(advancedRuleDescriptionSectionsGenerator.generateSections(rule))
.usingRecursiveFieldByFieldElementComparator()
.containsExactlyInAnyOrderElementsOf(testData.getExpectedRuleDescriptionSectionsDto());
}

}

+ 8
- 0
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java View File

@@ -37,7 +37,11 @@ import org.sonar.server.qualityprofile.builtin.BuiltInQProfileLoader;
import org.sonar.server.qualityprofile.builtin.BuiltInQProfileUpdateImpl;
import org.sonar.server.qualityprofile.builtin.BuiltInQualityProfilesUpdateListener;
import org.sonar.server.qualityprofile.RegisterQualityProfiles;
import org.sonar.server.rule.AdvancedRuleDescriptionSectionsGenerator;
import org.sonar.server.rule.LegacyHotspotRuleDescriptionSectionsGenerator;
import org.sonar.server.rule.LegacyIssueRuleDescriptionSectionsGenerator;
import org.sonar.server.rule.RegisterRules;
import org.sonar.server.rule.RuleDescriptionSectionsGeneratorResolver;
import org.sonar.server.rule.WebServerRuleFinder;
import org.sonar.server.startup.GeneratePluginIndex;
import org.sonar.server.startup.RegisterMetrics;
@@ -65,6 +69,10 @@ public class PlatformLevelStartup extends PlatformLevel {
addIfStartupLeaderAndPluginsChanged(
RegisterMetrics.class,
RegisterQualityGates.class,
RuleDescriptionSectionsGeneratorResolver.class,
AdvancedRuleDescriptionSectionsGenerator.class,
LegacyHotspotRuleDescriptionSectionsGenerator.class,
LegacyIssueRuleDescriptionSectionsGenerator.class,
RegisterRules.class,
BuiltInQProfileLoader.class);
addIfStartupLeader(

Loading…
Cancel
Save