Explorar el Código

SONAR-16518 support contextual section descriptions in /api/rules/search and /api/rules/show + adapt ES indexation

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 description
tags/9.6.0.59041
Aurelien Poscia hace 1 año
padre
commit
923ec14b8e

+ 5
- 1
server/sonar-server-common/src/main/java/org/sonar/server/rule/RuleDescriptionFormatter.java Ver fichero

@@ -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 {

+ 20
- 3
server/sonar-server-common/src/test/java/org/sonar/server/rule/RuleDescriptionFormatterTest.java Ver fichero

@@ -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();
}
}

+ 12
- 1
server/sonar-server-common/src/test/java/org/sonar/server/rule/index/RuleDocTest.java Ver fichero

@@ -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();
}
}

+ 11
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/RuleMapper.java Ver fichero

@@ -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;
}

+ 15
- 25
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/SearchAction.java Ver fichero

@@ -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);

+ 16
- 17
server/sonar-webserver-webapi/src/main/java/org/sonar/server/rule/ws/ShowAction.java Ver fichero

@@ -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)

+ 13
- 3
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/search-example.json Ver fichero

@@ -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": [

+ 13
- 3
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/rule/ws/show-example.json Ver fichero

@@ -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": [

+ 65
- 4
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/SearchActionTest.java Ver fichero

@@ -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);
}

+ 33
- 17
server/sonar-webserver-webapi/src/test/java/org/sonar/server/rule/ws/ShowActionTest.java Ver fichero

@@ -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) {

+ 5
- 0
sonar-ws/src/main/protobuf/ws-rules.proto Ver fichero

@@ -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 {

Cargando…
Cancelar
Guardar