]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10544 Set effort for external issues
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Mon, 23 Apr 2018 09:45:03 +0000 (11:45 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 26 Apr 2018 18:20:53 +0000 (20:20 +0200)
17 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/DebtCalculator.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackerRawInputFactory.java
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/commonrule/CommonRule.java
server/sonar-server/src/main/java/org/sonar/server/rule/ExternalRuleCreator.java
server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/DebtCalculatorTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/TransitionActionTest.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
sonar-scanner-engine/src/main/java/org/sonar/scanner/externalissue/ExternalIssueImporter.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/externalissue/ReportParserTest.java
sonar-scanner-engine/src/test/java/org/sonar/scanner/mediumtest/issues/ExternalIssuesMediumTest.java
sonar-scanner-engine/src/test/resources/mediumtest/xoo/sample/externalIssues.json
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report.json
sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json [new file with mode: 0644]
tests/projects/shared/xoo-sample/externalIssues.json [new file with mode: 0644]
tests/src/test/java/org/sonarqube/tests/issue/ExternalIssueTest.java

index 0213e3106e5adcc44548b720dd977ca203086c1d..3f72a51740afdc204b9084453f0fbc644afd1306 100644 (file)
@@ -733,7 +733,7 @@ public final class IssueDto implements Serializable {
     issue.setUpdateDate(longToDate(issueUpdateDate));
     issue.setSelectedAt(selectedAt);
     issue.setLocations(parseLocations());
-    issue.setFromExternalRuleEngine(isExternal);
+    issue.setIsFromExternalRuleEngine(isExternal);
     return issue;
   }
 }
index 328447ed02a3b2387e56aed39a7c30135db00fa4..c896d0de5843b9202397f88714ceda904db43631 100644 (file)
@@ -40,19 +40,22 @@ public class DebtCalculator {
 
   @CheckForNull
   public Duration calculate(DefaultIssue issue) {
+    if (issue.isFromExternalRuleEngine()) {
+      return issue.effort();
+    }
     Rule rule = ruleRepository.getByKey(issue.ruleKey());
     DebtRemediationFunction fn = rule.getRemediationFunction();
     if (fn != null) {
       verifyEffortToFix(issue, fn);
 
       Duration debt = Duration.create(0);
-      String gapMultiplier =fn.gapMultiplier();
+      String gapMultiplier = fn.gapMultiplier();
       if (fn.type().usesGapMultiplier() && !Strings.isNullOrEmpty(gapMultiplier)) {
-        int effortToFixValue = MoreObjects.firstNonNull(issue.effortToFix(), 1).intValue();
+        int effortToFixValue = MoreObjects.firstNonNull(issue.gap(), 1).intValue();
         // TODO convert to Duration directly in Rule#remediationFunction -> better performance + error handling
         debt = durations.decode(gapMultiplier).multiply(effortToFixValue);
       }
-      String baseEffort= fn.baseEffort();
+      String baseEffort = fn.baseEffort();
       if (fn.type().usesBaseEffort() && !Strings.isNullOrEmpty(baseEffort)) {
         // TODO convert to Duration directly in Rule#remediationFunction -> better performance + error handling
         debt = debt.add(durations.decode(baseEffort));
@@ -63,7 +66,7 @@ public class DebtCalculator {
   }
 
   private static void verifyEffortToFix(DefaultIssue issue, DebtRemediationFunction fn) {
-    if (Type.CONSTANT_ISSUE.equals(fn.type()) && issue.effortToFix() != null) {
+    if (Type.CONSTANT_ISSUE.equals(fn.type()) && issue.gap() != null) {
       throw new IllegalArgumentException("Rule '" + issue.getRuleKey() + "' can not use 'Constant/issue' remediation function " +
         "because this rule does not have a fixed remediation cost.");
     }
index 8b134cf9d18ac49ec57bf6e05cb47e88934527d8..5344410c4590c55ac4f4c55918defb4819550320 100644 (file)
@@ -167,7 +167,7 @@ public class TrackerRawInputFactory {
           dbLocationsBuilder.addFlow(dbFlowBuilder);
         }
       }
-      issue.setFromExternalRuleEngine(false);
+      issue.setIsFromExternalRuleEngine(false);
       issue.setLocations(dbLocationsBuilder.build());
       return issue;
     }
@@ -204,7 +204,7 @@ public class TrackerRawInputFactory {
           dbLocationsBuilder.addFlow(dbFlowBuilder);
         }
       }
-      issue.setFromExternalRuleEngine(true);
+      issue.setIsFromExternalRuleEngine(true);
       issue.setLocations(dbLocationsBuilder.build());
       issue.setType(toRuleType(reportIssue.getType()));
 
index 9f1a84bafaaf2a00ccff9bf1c77d4497ceb471cc..397f9305355676aa1ab874315f8fb6ac790a8709 100644 (file)
@@ -55,7 +55,7 @@ public abstract class CommonRule {
         issue.setSeverity(activeRule.get().getSeverity());
         issue.setLine(null);
         issue.setChecksum("");
-        issue.setFromExternalRuleEngine(false);
+        issue.setIsFromExternalRuleEngine(false);
       }
     }
     return issue;
index 1f96b689c94d4f17e2f25c9c0d243606a41074f0..30ee19195639336489aa504ca33a0f3d3ca0b694 100644 (file)
@@ -61,7 +61,6 @@ public class ExternalRuleCreator {
       .setUpdatedAt(system2.now()));
 
     Rule newRule = new RuleImpl(dao.selectOrFailByKey(dbSession, external.getKey()));
-    // TODO write rule repository if needed
     ruleIndexer.commitAndIndex(dbSession, newRule.getId());
     return newRule;
   }
index f7154ba9de7ff1e34f761b7799dcce99d397bed1..67a9d2ebd119f73fe6185161542745c181a8669a 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.computation.task.projectanalysis.issue;
 import org.junit.Test;
 import org.sonar.api.server.debt.DebtRemediationFunction;
 import org.sonar.api.server.debt.internal.DefaultDebtRemediationFunction;
+import org.sonar.api.utils.Duration;
 import org.sonar.api.utils.Durations;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.db.rule.RuleTesting;
@@ -72,6 +73,16 @@ public class DebtCalculatorTest {
     assertThat(underTest.calculate(issue).toMinutes()).isEqualTo((int) (coefficient * effortToFix));
   }
 
+  @Test
+  public void copy_effort_for_external_issues() {
+    issue.setGap(null);
+    issue.setIsFromExternalRuleEngine(true);
+    issue.setEffort(Duration.create(20l));
+    rule.setFunction(null);
+
+    assertThat(underTest.calculate(issue).toMinutes()).isEqualTo(20l);
+  }
+
   @Test
   public void constant_function() {
     int constant = 2;
index 6ada52d8ace36c5e7c2adbfcd418688afe107a72..cc5c1b113a02520cc6e20ace3511d55966f4d68f 100644 (file)
@@ -117,7 +117,7 @@ public class TransitionActionTest {
     loginAndAddProjectPermission("john", ISSUE_ADMIN);
 
     context.issue()
-      .setFromExternalRuleEngine(true)
+      .setIsFromExternalRuleEngine(true)
       .setStatus(STATUS_CLOSED);
 
     action.execute(ImmutableMap.of("transition", "close"), context);
index 0add84d5a82164701bce0c95f7a53fa4bb106810..f1339f27a69263debd11c0f3f936f00c868007d7 100644 (file)
@@ -245,8 +245,8 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure.
     return isFromExternalRuleEngine;
   }
 
-  public DefaultIssue setFromExternalRuleEngine(boolean fromExternalRuleEngine) {
-    isFromExternalRuleEngine = fromExternalRuleEngine;
+  public DefaultIssue setIsFromExternalRuleEngine(boolean isFromExternalRuleEngine) {
+    this.isFromExternalRuleEngine = isFromExternalRuleEngine;
     return this;
   }
 
index e302c0bd3e321e11382e0b9f521b8ae7bb33647f..7f5c4fdf8069f3f103cb67092e9638871cb6e242 100644 (file)
@@ -170,7 +170,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder {
     issue.setCopied(false);
     issue.setBeingClosed(false);
     issue.setOnDisabledRule(false);
-    issue.setFromExternalRuleEngine(isFromExternalRuleEngine);
+    issue.setIsFromExternalRuleEngine(isFromExternalRuleEngine);
     return issue;
   }
 }
index 4a5e35e96f43fb1cbae45c1ecd258cc11c79506f..99100318b688e02efb289138249b07ccab918410 100644 (file)
@@ -107,9 +107,12 @@ public class ExternalIssueImporter {
     InputFile file = findFile(context, location.filePath);
     if (file != null) {
       newLocation
-        .message(location.message)
         .on(file);
 
+      if (location.message != null) {
+        newLocation.message(location.message);
+      }
+
       if (location.textRange != null) {
         if (location.textRange.startColumn != null) {
           TextPointer start = file.newPointer(location.textRange.startLine, location.textRange.startColumn);
index e13e64d6b85d218ac42a9dbeadf8051c1de035ab..add4404b2a9f50c1ba7f22bf7e10b8fcbe6d503b 100644 (file)
@@ -39,7 +39,7 @@ public class ReportParserTest {
     System.out.println(Paths.get("org/sonar/scanner/externalissue/report.json").toAbsolutePath());
     Report report = parser.parse();
 
-    assertThat(report.issues).hasSize(3);
+    assertThat(report.issues).hasSize(4);
     assertThat(report.issues[0].engineId).isEqualTo("eslint");
     assertThat(report.issues[0].ruleId).isEqualTo("rule1");
     assertThat(report.issues[0].severity).isEqualTo("MAJOR");
@@ -52,6 +52,19 @@ public class ReportParserTest {
     assertThat(report.issues[0].primaryLocation.textRange.endColumn).isEqualTo(4);
     assertThat(report.issues[0].primaryLocation.textRange.endLine).isEqualTo(3);
     assertThat(report.issues[0].secondaryLocations).isNull();
+
+    assertThat(report.issues[3].engineId).isEqualTo("eslint");
+    assertThat(report.issues[3].ruleId).isEqualTo("rule3");
+    assertThat(report.issues[3].severity).isEqualTo("MAJOR");
+    assertThat(report.issues[3].effortMinutes).isNull();
+    assertThat(report.issues[3].type).isEqualTo("BUG");
+    assertThat(report.issues[3].secondaryLocations).hasSize(2);
+    assertThat(report.issues[3].secondaryLocations[0].filePath).isEqualTo("file1.js");
+    assertThat(report.issues[3].secondaryLocations[0].message).isEqualTo("fix the bug here");
+    assertThat(report.issues[3].secondaryLocations[0].textRange.startLine).isEqualTo(1);
+    assertThat(report.issues[3].secondaryLocations[1].filePath).isEqualTo("file2.js");
+    assertThat(report.issues[3].secondaryLocations[1].message).isNull();
+    assertThat(report.issues[3].secondaryLocations[1].textRange.startLine).isEqualTo(2);
   }
 
   private Path path(String reportName) {
@@ -121,4 +134,12 @@ public class ReportParserTest {
     exception.expectMessage("missing mandatory field 'filePath'");
     parser.parse();
   }
+  
+  @Test
+  public void fail_if_message_not_set_in_primaryLocation() {
+    ReportParser parser = new ReportParser(path("report_missing_message.json"));
+    exception.expect(IllegalStateException.class);
+    exception.expectMessage("missing mandatory field 'message'");
+    parser.parse();
+  }
 }
index 6d406300c3b74495ca816ee3066debf848cc68bc..92e01b6491df2258935a0e0b21afad211f326dd4 100644 (file)
@@ -28,7 +28,6 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 import org.sonar.api.utils.log.LogTester;
-import org.sonar.api.utils.log.LoggersTest;
 import org.sonar.scanner.mediumtest.ScannerMediumTester;
 import org.sonar.scanner.mediumtest.TaskResult;
 import org.sonar.scanner.protocol.Constants.Severity;
@@ -111,18 +110,23 @@ public class ExternalIssuesMediumTest {
     assertThat(issue.getTextRange().getStartOffset()).isEqualTo(0);
     assertThat(issue.getTextRange().getEndOffset()).isEqualTo(24);
 
-    // One file-level issue in helloscala
+    // One file-level issue in helloscala, with secondary location
     List<ExternalIssue> externalIssues2 = result.externalIssuesFor(result.inputFile("xources/hello/helloscala.xoo"));
     assertThat(externalIssues2).hasSize(1);
 
     issue = externalIssues2.iterator().next();
-    assertThat(issue.getFlowCount()).isZero();
+    assertThat(issue.getFlowCount()).isEqualTo(2);
     assertThat(issue.getMsg()).isEqualTo("fix the bug here");
     assertThat(issue.getRuleKey()).isEqualTo("rule3");
     assertThat(issue.getSeverity()).isEqualTo(Severity.MAJOR);
     assertThat(issue.getType()).isEqualTo(IssueType.BUG);
     assertThat(issue.hasTextRange()).isFalse();
-    
+    assertThat(issue.getFlow(0).getLocationCount()).isOne();
+    assertThat(issue.getFlow(0).getLocation(0).getTextRange().getStartLine()).isOne();
+    assertThat(issue.getFlow(1).getLocationCount()).isOne();
+    assertThat(issue.getFlow(1).getLocation(0).getTextRange().getStartLine()).isEqualTo(3);
+
+
     // one issue is located in a non-existing file
     assertThat(logs.logs()).contains("External issues ignored for 1 unknown files, including: invalidFile");
 
index 0e50bedd8dc1bf12fab23135057959792f2c0336..539533af78f8333095a82d6f310408860d755951 100644 (file)
     "primaryLocation": {
       "message": "fix the bug here",
       "filePath": "xources/hello/helloscala.xoo"
-    }     
+    },
+    "secondaryLocations": [
+      {
+        "filePath": "xources/hello/HelloJava.xoo",
+        "textRange": {
+          "startLine": 1
+        }
+      },
+      {
+        "filePath": "xources/hello/HelloJava.xoo",
+        "textRange": {
+          "startLine": 3
+        }
+      }
+    ]     
   }
 ]
 }
\ No newline at end of file
index 06627d7106b3d2f377916433eff6342d9102aed0..b951311436bdbec2d0aaea8d0aa5763e0c4013e2 100644 (file)
       "message": "fix the bug here",
       "filePath": "file3.js"
     }     
+  },
+  { 
+    "engineId": "eslint",
+    "ruleId": "rule3",
+    "severity": "MAJOR",
+    "type": "BUG",
+    "primaryLocation": {
+      "message": "fix the bug here",
+      "filePath": "file3.js"
+    },
+    "secondaryLocations": [
+      {
+        "message": "fix the bug here",
+        "filePath": "file1.js",
+        "textRange": {
+          "startLine": 1
+        }
+      },
+      {
+        "filePath": "file2.js",
+        "textRange": {
+          "startLine": 2
+        }
+      }
+    ]
   }
 ]
 }
diff --git a/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json b/sonar-scanner-engine/src/test/resources/org/sonar/scanner/externalissue/report_missing_message.json
new file mode 100644 (file)
index 0000000..c191559
--- /dev/null
@@ -0,0 +1,28 @@
+{
+"issues" : [
+  { 
+    "engineId": "eslint",
+    "ruleId": "rule1",
+    "severity": "MAJOR",
+    "type": "CODE_SMELL",
+    "primaryLocation": {
+      "message": "fix the issue here",
+      "filePath": "file1.js",
+      "textRange": {
+        "startLine": 1,
+        "endLine": 2
+      }
+    }     
+  },
+  { 
+    "engineId": "eslint",
+    "ruleId": "rule2",
+    "severity": "MAJOR",
+    "type": "BUG",
+    "primaryLocation": {
+      "filePath": "file1.js"
+    }     
+  }
+]
+}
+   
diff --git a/tests/projects/shared/xoo-sample/externalIssues.json b/tests/projects/shared/xoo-sample/externalIssues.json
new file mode 100644 (file)
index 0000000..9be94f4
--- /dev/null
@@ -0,0 +1,45 @@
+{
+"issues" : [
+  { 
+    "engineId": "externalXoo",
+    "ruleId": "rule1",
+    "severity": "MAJOR",
+    "type": "CODE_SMELL",
+    "effortMinutes": 50,
+    "primaryLocation": {
+      "message": "fix the issue here",
+      "filePath": "src/main/xoo/sample/Sample.xoo",
+      "textRange": {
+        "startLine": 5,
+        "startColumn": 2,
+        "endLine": 5,
+        "endColumn": 21
+      }
+    }     
+  },
+  { 
+    "engineId": "externalXoo",
+    "ruleId": "rule2",
+    "severity": "CRITICAL",
+    "type": "BUG",
+    "primaryLocation": {
+      "message": "fix the bug here",
+      "filePath": "src/main/xoo/sample/Sample.xoo"
+    },
+    "secondaryLocations": [
+      {
+        "filePath": "src/main/xoo/sample/Sample.xoo",
+        "textRange": {
+          "startLine": 1
+        }
+      },
+      {
+        "filePath": "unknown",
+        "textRange": {
+          "startLine": 3
+        }
+      }
+    ]     
+  }
+]
+}
\ No newline at end of file
index d8d4677560ca8507c4785d509f8e357671e7ac83..4a1f4b91a7b139b7d9a0cff9c60036059d1f08d3 100644 (file)
  */
 package org.sonarqube.tests.issue;
 
-import com.sonar.orchestrator.build.SonarScanner;
+import com.sonar.orchestrator.Orchestrator;
+import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 import org.junit.Before;
+import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonarqube.qa.util.Tester;
@@ -35,9 +36,15 @@ import util.ItUtils;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
-public class ExternalIssueTest extends AbstractIssueTest {
+public class ExternalIssueTest {
   private static final String PROJECT_KEY = "project";
 
+  // This class uses its own instance of the server because it creates external rules in it
+  @ClassRule
+  public static final Orchestrator ORCHESTRATOR = ItUtils.newOrchestratorBuilder()
+    .addPlugin(ItUtils.xooPlugin())
+    .build();
+
   @Rule
   public Tester tester = new Tester(ORCHESTRATOR);
 
@@ -50,45 +57,95 @@ public class ExternalIssueTest extends AbstractIssueTest {
 
   @Test
   public void should_import_external_issues_and_create_external_rules() {
-    noExternalRuleAndNoIssues();
+    noIssues();
+    ruleDoesntExist("external_xoo:OneExternalIssuePerLine");
 
-    SonarScanner sonarScanner = ItUtils.runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample",
+    ItUtils.runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample",
       "sonar.oneExternalIssuePerLine.activate", "true");
     List<Issue> issuesList = tester.wsClient().issues().search(new SearchRequest()).getIssuesList();
     assertThat(issuesList).hasSize(17);
 
     assertThat(issuesList).allMatch(issue -> "external_xoo:OneExternalIssuePerLine".equals(issue.getRule()));
     assertThat(issuesList).allMatch(issue -> "This issue is generated on each line".equals(issue.getMessage()));
-    assertThat(issuesList).allMatch(issue -> "This issue is generated on each line".equals(issue.getMessage()));
     assertThat(issuesList).allMatch(issue -> Severity.MAJOR.equals(issue.getSeverity()));
-    assertThat(issuesList).allMatch(issue -> RuleType.CODE_SMELL.equals(issue.getType()));
+    assertThat(issuesList).allMatch(issue -> RuleType.BUG.equals(issue.getType()));
     assertThat(issuesList).allMatch(issue -> "sample:src/main/xoo/sample/Sample.xoo".equals(issue.getComponent()));
     assertThat(issuesList).allMatch(issue -> "OPEN".equals(issue.getStatus()));
     assertThat(issuesList).allMatch(issue -> issue.getExternalRuleEngine().equals("xoo"));
 
-    List<org.sonarqube.ws.Rules.Rule> rulesList = tester.wsClient().rules()
-      .search(new org.sonarqube.ws.client.rules.SearchRequest().setIsExternal(Boolean.toString(true))).getRulesList();
-    List<org.sonarqube.ws.Rules.Rule> externalRules = rulesList.stream().filter(rule -> rule.getIsExternal()).collect(Collectors.toList());
-
-    assertThat(externalRules).hasSize(1);
-    assertThat(externalRules.get(0).getKey()).isEqualTo("external_xoo:OneExternalIssuePerLine");
-    assertThat(externalRules.get(0).getIsTemplate()).isFalse();
-    assertThat(externalRules.get(0).getIsExternal()).isTrue();
-    assertThat(externalRules.get(0).getTags().getTagsCount()).isEqualTo(0);
-    assertThat(externalRules.get(0).getScope()).isEqualTo(RuleScope.ALL);
+    ruleExists("external_xoo:OneExternalIssuePerLine");
 
     // second analysis, issue tracking should work
-    sonarScanner = ItUtils.runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample",
+    ItUtils.runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample",
       "sonar.oneExternalIssuePerLine.activate", "true");
     issuesList = tester.wsClient().issues().search(new SearchRequest()).getIssuesList();
     assertThat(issuesList).hasSize(17);
   }
 
-  private void noExternalRuleAndNoIssues() {
+  @Test
+  public void should_import_external_issues_from_json_report_and_create_external_rules() {
+    noIssues();
+    ruleDoesntExist("external_externalXoo:rule1");
+    ruleDoesntExist("external_externalXoo:rule2");
+
+    ItUtils.runProjectAnalysis(ORCHESTRATOR, "shared/xoo-sample",
+      "sonar.externalIssuesReportPaths", "externalIssues.json");
+
+    List<Issue> issuesList = tester.wsClient().issues().search(new SearchRequest()
+      .setRules(Collections.singletonList("external_externalXoo:rule1"))).getIssuesList();
+    assertThat(issuesList).hasSize(1);
+
+    assertThat(issuesList.get(0).getRule()).isEqualTo("external_externalXoo:rule1");
+    assertThat(issuesList.get(0).getMessage()).isEqualTo("fix the issue here");
+    assertThat(issuesList.get(0).getSeverity()).isEqualTo(Severity.MAJOR);
+    assertThat(issuesList.get(0).getType()).isEqualTo(RuleType.CODE_SMELL);
+    assertThat(issuesList.get(0).getComponent()).isEqualTo("sample:src/main/xoo/sample/Sample.xoo");
+    assertThat(issuesList.get(0).getStatus()).isEqualTo("OPEN");
+    assertThat(issuesList.get(0).getEffort()).isEqualTo("20min");
+    assertThat(issuesList.get(0).getExternalRuleEngine()).isEqualTo("externalXoo");
+
+    issuesList = tester.wsClient().issues().search(new SearchRequest()
+      .setRules(Collections.singletonList("external_externalXoo:rule2"))).getIssuesList();
+    assertThat(issuesList).hasSize(1);
+
+    assertThat(issuesList.get(0).getRule()).isEqualTo("external_externalXoo:rule2");
+    assertThat(issuesList.get(0).getMessage()).isEqualTo("fix the bug here");
+    assertThat(issuesList.get(0).getSeverity()).isEqualTo(Severity.CRITICAL);
+    assertThat(issuesList.get(0).getType()).isEqualTo(RuleType.BUG);
+    assertThat(issuesList.get(0).getComponent()).isEqualTo("sample:src/main/xoo/sample/Sample.xoo");
+    assertThat(issuesList.get(0).getStatus()).isEqualTo("OPEN");
+    assertThat(issuesList.get(0).getExternalRuleEngine()).isEqualTo("externalXoo");
+
+    ruleExists("external_externalXoo:rule1");
+    ruleExists("external_externalXoo:rule2");
+  }
+
+  private void ruleDoesntExist(String key) {
     List<org.sonarqube.ws.Rules.Rule> rulesList = tester.wsClient().rules()
-      .search(new org.sonarqube.ws.client.rules.SearchRequest().setIsExternal(Boolean.toString(true))).getRulesList();
-    assertThat(rulesList).noneMatch(rule -> rule.getIsExternal());
+      .search(new org.sonarqube.ws.client.rules.SearchRequest()
+        .setRuleKey(key)
+        .setIsExternal(Boolean.toString(true)))
+      .getRulesList();
+    assertThat(rulesList).isEmpty();
+
+  }
+
+  private void ruleExists(String key) {
+    List<org.sonarqube.ws.Rules.Rule> rulesList = tester.wsClient().rules()
+      .search(new org.sonarqube.ws.client.rules.SearchRequest()
+        .setRuleKey(key)
+        .setIsExternal(Boolean.toString(true)))
+      .getRulesList();
+
+    assertThat(rulesList).hasSize(1);
+    assertThat(rulesList.get(0).getKey()).isEqualTo(key);
+    assertThat(rulesList.get(0).getIsTemplate()).isFalse();
+    assertThat(rulesList.get(0).getIsExternal()).isTrue();
+    assertThat(rulesList.get(0).getTags().getTagsCount()).isEqualTo(0);
+    assertThat(rulesList.get(0).getScope()).isEqualTo(RuleScope.ALL);
+  }
 
+  private void noIssues() {
     List<Issue> issuesList = tester.wsClient().issues().search(new SearchRequest()).getIssuesList();
     assertThat(issuesList).isEmpty();
   }