SONAR-16518 update api/rules/search, api/rules/show documentation Update /api/rules/search and /api/rules/show to put change log in change logs entries rather than descriptiontags/9.6.0.59041
@@ -26,10 +26,12 @@ import java.util.Objects; | |||
import java.util.Optional; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.markdown.Markdown; | |||
import static java.util.Comparator.comparing; | |||
import static java.util.stream.Collectors.toMap; | |||
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; | |||
@@ -76,7 +78,9 @@ public class RuleDescriptionFormatter { | |||
return null; | |||
} | |||
Map<String, String> sectionKeyToHtml = ruleDescriptionSectionDtos.stream() | |||
.collect(toMap(RuleDescriptionSectionDto::getKey, section -> toHtml(descriptionFormat, section))); | |||
//TODO MMF-2765, merge operation on toMap should not be needed anymore | |||
.sorted(comparing(RuleDescriptionSectionDto::getKey).thenComparing(s -> Optional.ofNullable(s.getContext()).map(RuleDescriptionSectionContextDto::getKey).orElse(null))) | |||
.collect(toMap(RuleDescriptionSectionDto::getKey, section -> toHtml(descriptionFormat, section), (k1, k2) -> k1)); | |||
if (sectionKeyToHtml.containsKey(DEFAULT_KEY)) { | |||
return sectionKeyToHtml.get(DEFAULT_KEY); | |||
} else { |
@@ -19,8 +19,12 @@ | |||
*/ | |||
package org.sonar.server.rule; | |||
import java.util.Optional; | |||
import org.apache.commons.lang.RandomStringUtils; | |||
import org.jetbrains.annotations.Nullable; | |||
import org.junit.Test; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
@@ -28,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; | |||
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.INTRODUCTION_SECTION_KEY; | |||
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY; | |||
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY; | |||
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; | |||
@@ -57,15 +62,19 @@ public class RuleDescriptionFormatterTest { | |||
var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>"); | |||
var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>"); | |||
var section4 = createRuleDescriptionSection(INTRODUCTION_SECTION_KEY, "<div>Introduction with no title</div>"); | |||
var section5ctx1 = createRuleDescriptionSection(RESOURCES_SECTION_KEY, "<div>CTX_1</div>", "CTX_1"); | |||
var section5ctx2 = createRuleDescriptionSection(RESOURCES_SECTION_KEY, "<div>CTX_2</div>", "CTX_2"); | |||
RuleDto rule = new RuleDto().setDescriptionFormat(RuleDto.Format.HTML) | |||
.setType(RuleType.SECURITY_HOTSPOT) | |||
.addRuleDescriptionSectionDto(section1) | |||
.addRuleDescriptionSectionDto(section2) | |||
.addRuleDescriptionSectionDto(section3) | |||
.addRuleDescriptionSectionDto(section4); | |||
.addRuleDescriptionSectionDto(section4) | |||
.addRuleDescriptionSectionDto(section5ctx2) | |||
.addRuleDescriptionSectionDto(section5ctx1); | |||
String html = ruleDescriptionFormatter.getDescriptionAsHtml(rule); | |||
assertThat(html) | |||
.contains( | |||
.isEqualTo( | |||
"<div>Introduction with no title</div><br/>" | |||
+ "<h2>What is the risk?</h2>" | |||
+ "<div>Root is Root</div><br/>" | |||
@@ -73,6 +82,7 @@ public class RuleDescriptionFormatterTest { | |||
+ "<div>This is not a problem</div><br/>" | |||
+ "<h2>How can you fix it?</h2>" | |||
+ "<div>I don't want to fix</div><br/>" | |||
+ "<div>CTX_1</div><br/>" | |||
); | |||
} | |||
@@ -92,6 +102,13 @@ public class RuleDescriptionFormatterTest { | |||
} | |||
private static RuleDescriptionSectionDto createRuleDescriptionSection(String key, String content) { | |||
return RuleDescriptionSectionDto.builder().key(key).content(content).build(); | |||
return createRuleDescriptionSection(key, content, null); | |||
} | |||
private static RuleDescriptionSectionDto createRuleDescriptionSection(String key, String content, @Nullable String contextKey) { | |||
RuleDescriptionSectionContextDto context = Optional.ofNullable(contextKey) | |||
.map(c -> RuleDescriptionSectionContextDto.of(contextKey, contextKey + RandomStringUtils.randomAlphanumeric(20))) | |||
.orElse(null); | |||
return RuleDescriptionSectionDto.builder().key(key).content(content).context(context).build(); | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.rule.index; | |||
import org.junit.Test; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleForIndexingDto; | |||
@@ -89,8 +90,12 @@ public class RuleDocTest { | |||
ruleDto.getRuleDescriptionSectionDtos().clear(); | |||
RuleDescriptionSectionDto section1 = buildRuleDescriptionSectionDto("section1", "<p>html content 1</p>"); | |||
RuleDescriptionSectionDto section2 = buildRuleDescriptionSectionDto("section2", "<p>html content 2</p>"); | |||
RuleDescriptionSectionDto section3ctx1 = buildRuleDescriptionSectionDtoWithContext("section3", "<p>html content 3.1</p>", "ctx1"); | |||
RuleDescriptionSectionDto section3ctx2 = buildRuleDescriptionSectionDtoWithContext("section3", "<p>html content 3.2</p>", "ctx2"); | |||
ruleDto.addRuleDescriptionSectionDto(section1); | |||
ruleDto.addRuleDescriptionSectionDto(section2); | |||
ruleDto.addRuleDescriptionSectionDto(section3ctx1); | |||
ruleDto.addRuleDescriptionSectionDto(section3ctx2); | |||
RuleForIndexingDto ruleForIndexingDto = RuleForIndexingDto.fromRuleDto(ruleDto); | |||
SecurityStandards securityStandards = fromSecurityStandards(ruleDto.getSecurityStandards()); | |||
@@ -99,7 +104,9 @@ public class RuleDocTest { | |||
assertThat(ruleDoc.htmlDescription()) | |||
.contains(section1.getContent()) | |||
.contains(section2.getContent()) | |||
.hasSameSizeAs(section1.getContent() + " " + section2.getContent()); | |||
.contains(section3ctx1.getContent()) | |||
.contains(section3ctx2.getContent()) | |||
.hasSameSizeAs(section1.getContent() + " " + section2.getContent() + " " + section3ctx1.getContent() + " " + section3ctx2.getContent()); | |||
} | |||
@Test | |||
@@ -125,4 +132,8 @@ public class RuleDocTest { | |||
private static RuleDescriptionSectionDto buildRuleDescriptionSectionDto(String key, String content) { | |||
return RuleDescriptionSectionDto.builder().key(key).content(content).build(); | |||
} | |||
private static RuleDescriptionSectionDto buildRuleDescriptionSectionDtoWithContext(String key, String content, String contextKey) { | |||
return RuleDescriptionSectionDto.builder().key(key).content(content).context(RuleDescriptionSectionContextDto.of(contextKey, contextKey)).build(); | |||
} | |||
} |
@@ -35,6 +35,7 @@ import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.server.debt.DebtRemediationFunction; | |||
import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction; | |||
import org.sonar.db.rule.DeprecatedRuleKeyDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleDto.Scope; | |||
@@ -79,6 +80,7 @@ import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_STATUS; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_SYSTEM_TAGS; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_TAGS; | |||
import static org.sonar.server.rule.ws.RulesWsParameters.FIELD_TEMPLATE_KEY; | |||
import static org.sonarqube.ws.Rules.Rule.DescriptionSection.Context.newBuilder; | |||
/** | |||
* Conversion of {@link RuleDto} to {@link Rules.Rule} | |||
@@ -339,10 +341,11 @@ public class RuleMapper { | |||
if (shouldReturnField(fieldsToReturn, FIELD_DESCRIPTION_SECTIONS)) { | |||
for (var section : ruleDto.getRuleDescriptionSectionDtos()) { | |||
ruleResponse.getDescriptionSectionsBuilder().addDescriptionSectionsBuilder() | |||
Rules.Rule.DescriptionSection.Builder sectionBuilder = ruleResponse.getDescriptionSectionsBuilder().addDescriptionSectionsBuilder() | |||
.setKey(section.getKey()) | |||
.setContent(retrieveDescriptionContent(ruleDto.getDescriptionFormat(), section)) | |||
.build(); | |||
.setContent(retrieveDescriptionContent(ruleDto.getDescriptionFormat(), section)); | |||
toProtobufContext(section.getContext()).ifPresent(sectionBuilder::setContext); | |||
sectionBuilder.build(); | |||
} | |||
} | |||
@@ -430,6 +433,11 @@ public class RuleMapper { | |||
return fieldsToReturn.isEmpty() || fieldsToReturn.contains(fieldName); | |||
} | |||
private static Optional<Rules.Rule.DescriptionSection.Context> toProtobufContext(@Nullable RuleDescriptionSectionContextDto context) { | |||
return Optional.ofNullable(context) | |||
.map(c -> newBuilder().setDisplayName(c.getDisplayName()).build()); | |||
} | |||
private static boolean isRemediationFunctionOverloaded(RuleDto rule) { | |||
return rule.getRemediationFunction() != null; | |||
} |
@@ -137,11 +137,22 @@ public class SearchAction implements RulesWsAction { | |||
WebService.NewAction action = controller.createAction(ACTION) | |||
.addPagingParams(100, MAX_PAGE_SIZE) | |||
.setHandler(this) | |||
.setChangelog(new Change("7.1", "The field 'scope' has been added to the response"), | |||
.setChangelog( | |||
new Change("5.5", "The field 'effortToFixDescription' has been deprecated use 'gapDescription' instead"), | |||
new Change("5.5", "The field 'debtRemFnCoeff' has been deprecated use 'remFnGapMultiplier' instead"), | |||
new Change("5.5", "The field 'defaultDebtRemFnCoeff' has been deprecated use 'defaultRemFnGapMultiplier' instead"), | |||
new Change("5.5", "The field 'debtRemFnOffset' has been deprecated use 'remFnBaseEffort' instead"), | |||
new Change("5.5", "The field 'defaultDebtRemFnOffset' has been deprecated use 'defaultRemFnBaseEffort' instead"), | |||
new Change("7.1", "The field 'scope' has been added to the response"), | |||
new Change("7.1", "The field 'scope' has been added to the 'f' parameter"), | |||
new Change("7.2", "The field 'isExternal' has been added to the response"), | |||
new Change("7.2", "The field 'includeExternal' has been added to the 'f' parameter"), | |||
new Change("7.5", "The field 'updatedAt' has been added to the 'f' parameter")); | |||
new Change("7.5", "The field 'updatedAt' has been added to the 'f' parameter"), | |||
new Change("9.5", "The field 'htmlDesc' has been deprecated use 'descriptionSections' instead"), | |||
new Change("9.5", "The field 'descriptionSections' has been added to the payload"), | |||
new Change("9.5", "The field 'descriptionSections' has been added to the 'f' parameter"), | |||
new Change("9.6", "'descriptionSections' can optionally embed a context field") | |||
); | |||
action.createParam(FACETS) | |||
.setDescription("Comma-separated list of the facets to be computed. No facet is computed by default.") | |||
@@ -149,33 +160,12 @@ public class SearchAction implements RulesWsAction { | |||
.setExampleValue(format("%s,%s", POSSIBLE_FACETS[0], POSSIBLE_FACETS[1])); | |||
WebService.NewParam paramFields = action.createParam(FIELDS) | |||
.setDescription("Comma-separated list of additional fields to be returned in the response. All the fields are returned by default, except actives." + | |||
"Since 5.5, following fields have been deprecated :" + | |||
"<ul>" + | |||
"<li>\"defaultDebtRemFn\" becomes \"defaultRemFn\"</li>" + | |||
"<li>\"debtRemFn\" becomes \"remFn\"</li>" + | |||
"<li>\"effortToFixDescription\" becomes \"gapDescription\"</li>" + | |||
"<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" + | |||
"</ul>") | |||
.setDescription("Comma-separated list of additional fields to be returned in the response. All the fields are returned by default, except actives.") | |||
.setPossibleValues(Ordering.natural().sortedCopy(OPTIONAL_FIELDS)); | |||
Iterator<String> it = OPTIONAL_FIELDS.iterator(); | |||
paramFields.setExampleValue(format("%s,%s", it.next(), it.next())); | |||
action.setDescription("Search for a collection of relevant rules matching a specified query.<br/>" + | |||
"Since 5.5, following fields in the response have been deprecated :" + | |||
"<ul>" + | |||
"<li>\"effortToFixDescription\" becomes \"gapDescription\"</li>" + | |||
"<li>\"debtRemFnCoeff\" becomes \"remFnGapMultiplier\"</li>" + | |||
"<li>\"defaultDebtRemFnCoeff\" becomes \"defaultRemFnGapMultiplier\"</li>" + | |||
"<li>\"debtRemFnOffset\" becomes \"remFnBaseEffort\"</li>" + | |||
"<li>\"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"</li>" + | |||
"<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" + | |||
"</ul><br/>" + | |||
"Since 9.5 :" + | |||
"<ul>" + | |||
"<li>the field \"htmlDesc\" has been deprecated.</li>" + | |||
"<li>the field \"descriptionSections\" has been added.</li>" + | |||
"</ul>") | |||
action.setDescription("Search for a collection of relevant rules matching a specified query.<br/>") | |||
.setResponseExample(getClass().getResource("search-example.json")) | |||
.setSince("4.4") | |||
.setHandler(this); |
@@ -23,6 +23,7 @@ import com.google.common.io.Resources; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
import org.sonar.api.server.ws.Response; | |||
import org.sonar.api.server.ws.WebService; | |||
@@ -63,25 +64,23 @@ public class ShowAction implements RulesWsAction { | |||
public void define(WebService.NewController controller) { | |||
WebService.NewAction action = controller | |||
.createAction("show") | |||
.setDescription("Get detailed information about a rule<br>" + | |||
"Since 5.5, following fields in the response have been deprecated :" + | |||
"<ul>" + | |||
"<li>\"effortToFixDescription\" becomes \"gapDescription\"</li>" + | |||
"<li>\"debtRemFnCoeff\" becomes \"remFnGapMultiplier\"</li>" + | |||
"<li>\"defaultDebtRemFnCoeff\" becomes \"defaultRemFnGapMultiplier\"</li>" + | |||
"<li>\"debtRemFnOffset\" becomes \"remFnBaseEffort\"</li>" + | |||
"<li>\"defaultDebtRemFnOffset\" becomes \"defaultRemFnBaseEffort\"</li>" + | |||
"<li>\"debtOverloaded\" becomes \"remFnOverloaded\"</li>" + | |||
"</ul>" + | |||
"In 7.1, the field 'scope' has been added.<br/>" + | |||
"Since 9.5 :" + | |||
"<ul>" + | |||
"<li>the field \"htmlDesc\" has been deprecated.</li>" + | |||
"<li>the field \"descriptionSections\" has been added.</li>" + | |||
"</ul>") | |||
.setDescription("Get detailed information about a rule<br>") | |||
.setSince("4.2") | |||
.setResponseExample(Resources.getResource(getClass(), "show-example.json")) | |||
.setHandler(this); | |||
.setHandler(this) | |||
.setChangelog( | |||
new Change("5.5", "The field 'effortToFixDescription' has been deprecated use 'gapDescription' instead"), | |||
new Change("5.5", "The field 'debtRemFnCoeff' has been deprecated use 'remFnGapMultiplier' instead"), | |||
new Change("5.5", "The field 'defaultDebtRemFnCoeff' has been deprecated use 'defaultRemFnGapMultiplier' instead"), | |||
new Change("5.5", "The field 'debtRemFnOffset' has been deprecated use 'remFnBaseEffort' instead"), | |||
new Change("5.5", "The field 'defaultDebtRemFnOffset' has been deprecated use 'defaultRemFnBaseEffort' instead"), | |||
new Change("5.5", "The field 'debtOverloaded' has been deprecated use 'remFnOverloaded' instead"), | |||
new Change("7.5", "The field 'scope' has been added"), | |||
new Change("9.5", "The field 'htmlDesc' has been deprecated use 'descriptionSections' instead"), | |||
new Change("9.5", "The field 'descriptionSections' has been added to the payload"), | |||
new Change("9.5", "The field 'descriptionSections' has been added to the 'f' parameter"), | |||
new Change("9.6", "'descriptionSections' can optionally embed a context field") | |||
); | |||
action | |||
.createParam(PARAM_KEY) |
@@ -23,12 +23,22 @@ | |||
"type": "CODE_SMELL", | |||
"descriptionSections": [ | |||
{ | |||
"key": "Why is this an issue ?", | |||
"key": "root_cause", | |||
"content": "<h3 class=\"page-title coding-rules-detail-header\"><big>Unnecessary imports should be removed</big></h3>" | |||
}, | |||
{ | |||
"key": "How to fix it ?", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>" | |||
"key": "how_to_fix", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>", | |||
"context": { | |||
"displayName": "Spring" | |||
} | |||
}, | |||
{ | |||
"key": "how_to_fix", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate hibernate protection. </li></ul>", | |||
"context": { | |||
"displayName": "Hibernate" | |||
} | |||
} | |||
], | |||
"params": [ |
@@ -24,12 +24,22 @@ | |||
"type": "CODE_SMELL", | |||
"descriptionSections": [ | |||
{ | |||
"key": "Why is this an issue ?", | |||
"key": "root_cause", | |||
"content": "<h3 class=\"page-title coding-rules-detail-header\"><big>Unnecessary imports should be removed</big></h3>" | |||
}, | |||
{ | |||
"key": "How to fix it ?", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>" | |||
"key": "how_to_fix", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate Spring Security's CSRF protection. </li></ul>", | |||
"context": { | |||
"displayName": "Spring" | |||
} | |||
}, | |||
{ | |||
"key": "how_to_fix", | |||
"content": "<h2>Recommended Secure Coding Practices</h2><ul><li> activate hibernate protection. </li></ul>", | |||
"context": { | |||
"displayName": "Hibernate" | |||
} | |||
} | |||
], | |||
"params": [ |
@@ -21,10 +21,12 @@ package org.sonar.server.rule.ws; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.function.Function; | |||
import java.util.stream.Collectors; | |||
import org.assertj.core.api.iterable.Extractor; | |||
import javax.annotation.Nullable; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.impl.utils.AlwaysIncreasingSystem2; | |||
@@ -41,6 +43,8 @@ import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.qualityprofile.ActiveRuleParamDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleParamDto; | |||
import org.sonar.db.user.UserDto; | |||
@@ -83,6 +87,7 @@ import static org.mockito.ArgumentMatchers.anyString; | |||
import static org.mockito.Mockito.doReturn; | |||
import static org.mockito.Mockito.mock; | |||
import static org.sonar.api.rule.Severity.BLOCKER; | |||
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.RESOURCES_SECTION_KEY; | |||
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; | |||
import static org.sonar.db.rule.RuleTesting.newRuleWithoutDescriptionSection; | |||
import static org.sonar.db.rule.RuleTesting.setSystemTags; | |||
@@ -250,6 +255,23 @@ public class SearchActionTest { | |||
verify(r -> r.setParam("q", "now&forever"), rule1); | |||
} | |||
@Test | |||
public void filter_with_context_specific_rule_description() { | |||
RuleDescriptionSectionDto section1context1 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>I want to fix with Spring</div>", "ctx1"); | |||
RuleDescriptionSectionDto section1context2 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>Another context</div>", "ctx2"); | |||
RuleDto ruleDto = newRuleWithoutDescriptionSection() | |||
.setNoteUserUuid(null) | |||
.addRuleDescriptionSectionDto(section1context1) | |||
.addRuleDescriptionSectionDto(section1context2); | |||
db.rules().insert(ruleDto); | |||
indexRules(); | |||
verify(r -> r.setParam("q", "Spring "), ruleDto); | |||
verify(r -> r.setParam("q", "bold")); | |||
verify(r -> r.setParam("q", "context"), ruleDto); | |||
} | |||
@Test | |||
public void filter_by_rule_name_or_descriptions_requires_all_words_to_match_anywhere() { | |||
RuleDto rule1 = db.rules().insert(newRuleWithoutDescriptionSection().setName("Best rule ever").setNoteUserUuid(null) | |||
@@ -440,7 +462,13 @@ public class SearchActionTest { | |||
@Test | |||
public void should_return_specified_fields() { | |||
RuleDto rule = db.rules().insert(r1 -> r1.setLanguage("java")); | |||
RuleDescriptionSectionDto section1context1 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>I want to fix with Spring</div>", "ctx1"); | |||
RuleDescriptionSectionDto section1context2 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>Another context</div>", "ctx2"); | |||
RuleDto rule = newRuleWithoutDescriptionSection() | |||
.setLanguage("java") | |||
.addRuleDescriptionSectionDto(section1context1) | |||
.addRuleDescriptionSectionDto(section1context2); | |||
db.rules().insert(rule); | |||
indexRules(); | |||
checkField(rule, "repo", Rule::getRepo, rule.getRepositoryKey()); | |||
@@ -455,7 +483,30 @@ public class SearchActionTest { | |||
checkField(rule, "lang", Rule::getLang, rule.getLanguage()); | |||
checkField(rule, "langName", Rule::getLangName, languages.get(rule.getLanguage()).getName()); | |||
checkField(rule, "gapDescription", Rule::getGapDescription, rule.getGapDescription()); | |||
// to be continued... | |||
checkDescriptionSections(rule, rule.getRuleDescriptionSectionDtos().stream() | |||
.map(SearchActionTest::toProtobufDto) | |||
.collect(Collectors.toSet()) | |||
); | |||
} | |||
private RuleDescriptionSectionDto createRuleDescriptionSectionWithContext(String key, String content, @Nullable String contextKey) { | |||
RuleDescriptionSectionContextDto contextDto = Optional.ofNullable(contextKey) | |||
.map(c -> RuleDescriptionSectionContextDto.of(contextKey, contextKey + " display name")) | |||
.orElse(null); | |||
return RuleDescriptionSectionDto.builder() | |||
.uuid(uuidFactory.create()) | |||
.key(key) | |||
.content(content) | |||
.context(contextDto) | |||
.build(); | |||
} | |||
private static Rule.DescriptionSection toProtobufDto(RuleDescriptionSectionDto s) { | |||
Rule.DescriptionSection.Builder builder = Rule.DescriptionSection.newBuilder().setKey(s.getKey()).setContent(s.getContent()); | |||
if (s.getContext() != null) { | |||
builder.setContext(Rule.DescriptionSection.Context.newBuilder().setDisplayName(s.getContext().getDisplayName()).build()); | |||
} | |||
return builder.build(); | |||
} | |||
@Test | |||
@@ -909,7 +960,7 @@ public class SearchActionTest { | |||
} | |||
@SafeVarargs | |||
private final <T> void checkField(RuleDto rule, String fieldName, Extractor<Rule, T> responseExtractor, T... expected) { | |||
private <T> void checkField(RuleDto rule, String fieldName, Function<Rule, T> responseExtractor, T... expected) { | |||
SearchResponse result = ws.newRequest() | |||
.setParam("f", fieldName) | |||
.executeProtobuf(SearchResponse.class); | |||
@@ -917,6 +968,16 @@ public class SearchActionTest { | |||
assertThat(result.getRulesList()).extracting(responseExtractor).containsExactly(expected); | |||
} | |||
private void checkDescriptionSections(RuleDto rule, Set<Rule.DescriptionSection> expected) { | |||
SearchResponse result = ws.newRequest() | |||
.setParam("f", "descriptionSections") | |||
.executeProtobuf(SearchResponse.class); | |||
assertThat(result.getRulesList()).extracting(Rule::getKey).containsExactly(rule.getKey().toString()); | |||
List<Rule.DescriptionSection> actualSections = result.getRules(0).getDescriptionSections().getDescriptionSectionsList(); | |||
assertThat(actualSections).hasSameElementsAs(expected); | |||
} | |||
private void verifyNoResults(Consumer<TestRequest> requestPopulator) { | |||
verify(requestPopulator); | |||
} |
@@ -20,6 +20,8 @@ | |||
package org.sonar.server.rule.ws; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.resources.Languages; | |||
@@ -33,6 +35,7 @@ import org.sonar.db.DbTester; | |||
import org.sonar.db.qualityprofile.ActiveRuleDto; | |||
import org.sonar.db.qualityprofile.ActiveRuleParamDto; | |||
import org.sonar.db.qualityprofile.QProfileDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionContextDto; | |||
import org.sonar.db.rule.RuleDescriptionSectionDto; | |||
import org.sonar.db.rule.RuleDto; | |||
import org.sonar.db.rule.RuleParamDto; | |||
@@ -55,6 +58,7 @@ import static org.mockito.Mockito.verify; | |||
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.RESOURCES_SECTION_KEY; | |||
import static org.sonar.api.server.rule.RuleDescriptionSection.RuleDescriptionSectionKeys.ROOT_CAUSE_SECTION_KEY; | |||
import static org.sonar.db.rule.RuleDescriptionSectionDto.DEFAULT_KEY; | |||
import static org.sonar.db.rule.RuleDescriptionSectionDto.createDefaultRuleDescriptionSection; | |||
@@ -363,11 +367,14 @@ public class ShowActionTest { | |||
public void show_rule_desc_sections() { | |||
when(macroInterpreter.interpret(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); | |||
var section1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>"); | |||
var section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>"); | |||
var section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>"); | |||
RuleDescriptionSectionDto section1 = createRuleDescriptionSection(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>"); | |||
RuleDescriptionSectionDto section2 = createRuleDescriptionSection(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>"); | |||
RuleDescriptionSectionDto section3 = createRuleDescriptionSection(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>"); | |||
RuleDescriptionSectionDto section4context1 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>I want to fix with Spring</div>", "ctx1"); | |||
RuleDescriptionSectionDto section4context2 = createRuleDescriptionSectionWithContext(RESOURCES_SECTION_KEY, "<div>I want to fix with Servlet</div>", "ctx2"); | |||
RuleDto rule = createRuleWithDescriptionSections(section1, section2, section3); | |||
RuleDto rule = createRuleWithDescriptionSections(section1, section2, section3, section4context1, section4context2); | |||
rule.setType(RuleType.SECURITY_HOTSPOT); | |||
rule.setNoteUserUuid(userDto.getUuid()); | |||
db.rules().insert(rule); | |||
@@ -385,23 +392,20 @@ public class ShowActionTest { | |||
+ "<div>This is not a problem</div><br/>" | |||
+ "<h2>How can you fix it?</h2>" | |||
+ "<div>I don't want to fix</div><br/>" | |||
+ "<div>I want to fix with Spring</div>" | |||
); | |||
assertThat(resultRule.getMdDesc()) | |||
.contains( | |||
"<h2>What is the risk?</h2>" | |||
+ "<div>Root is Root</div><br/>" | |||
+ "<h2>Assess the risk</h2>" | |||
+ "<div>This is not a problem</div><br/>" | |||
+ "<h2>How can you fix it?</h2>" | |||
+ "<div>I don't want to fix</div><br/>"); | |||
assertThat(resultRule.getMdDesc()).isEqualTo(resultRule.getHtmlDesc()); | |||
assertThat(resultRule.getDescriptionSections().getDescriptionSectionsList()) | |||
.extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent) | |||
.extracting(Rule.DescriptionSection::getKey, Rule.DescriptionSection::getContent, section -> section.getContext().getDisplayName()) | |||
.containsExactlyInAnyOrder( | |||
tuple(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>"), | |||
tuple(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>"), | |||
tuple(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>")); | |||
tuple(ROOT_CAUSE_SECTION_KEY, "<div>Root is Root</div>", ""), | |||
tuple(ASSESS_THE_PROBLEM_SECTION_KEY, "<div>This is not a problem</div>", ""), | |||
tuple(HOW_TO_FIX_SECTION_KEY, "<div>I don't want to fix</div>", ""), | |||
tuple(RESOURCES_SECTION_KEY, "<div>I want to fix with Spring</div>", section4context1.getContext().getDisplayName()), | |||
tuple(RESOURCES_SECTION_KEY, "<div>I want to fix with Servlet</div>", section4context2.getContext().getDisplayName()) | |||
); | |||
} | |||
@Test | |||
@@ -538,7 +542,19 @@ public class ShowActionTest { | |||
} | |||
private RuleDescriptionSectionDto createRuleDescriptionSection(String key, String content) { | |||
return RuleDescriptionSectionDto.builder().uuid(uuidFactory.create()).key(key).content(content).build(); | |||
return createRuleDescriptionSectionWithContext(key, content, null); | |||
} | |||
private RuleDescriptionSectionDto createRuleDescriptionSectionWithContext(String key, String content, @Nullable String contextKey) { | |||
RuleDescriptionSectionContextDto contextDto = Optional.ofNullable(contextKey) | |||
.map(c -> RuleDescriptionSectionContextDto.of(contextKey, contextKey + " display name")) | |||
.orElse(null); | |||
return RuleDescriptionSectionDto.builder() | |||
.uuid(uuidFactory.create()) | |||
.key(key) | |||
.content(content) | |||
.context(contextDto) | |||
.build(); | |||
} | |||
private RuleDto createRuleWithDescriptionSections(RuleDescriptionSectionDto... sections) { |
@@ -131,6 +131,11 @@ message Rule { | |||
message DescriptionSection { | |||
required string key = 1; | |||
required string content = 2; | |||
optional Context context = 3; | |||
message Context { | |||
required string displayName = 1; | |||
} | |||
} | |||
message Params { |