3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.scanner.externalissue;
22 import com.google.gson.Gson;
23 import java.io.IOException;
24 import java.io.Reader;
25 import java.nio.charset.StandardCharsets;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import org.junit.Before;
30 import org.junit.Rule;
31 import org.junit.Test;
32 import org.slf4j.event.Level;
33 import org.sonar.api.testfixtures.log.LogAndArguments;
34 import org.sonar.api.testfixtures.log.LogTester;
35 import org.sonar.core.documentation.DocumentationLinkGenerator;
37 import static org.assertj.core.api.Assertions.assertThat;
38 import static org.assertj.core.api.Assertions.assertThatCode;
39 import static org.assertj.core.api.Assertions.assertThatThrownBy;
40 import static org.mockito.Mockito.mock;
41 import static org.mockito.Mockito.when;
43 public class ExternalIssueReportValidatorTest {
45 private static final String DEPRECATED_REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/";
46 private static final String REPORTS_LOCATION = "src/test/resources/org/sonar/scanner/externalissue/cct/";
47 private static final String URL = "/analyzing-source-code/importing-external-issues/generic-issue-import-format/";
48 private static final String TEST_URL = "test-url";
50 private final Gson gson = new Gson();
51 private final Path reportPath = Paths.get("report-path");
52 private final DocumentationLinkGenerator documentationLinkGenerator = mock(DocumentationLinkGenerator.class);
53 private final ExternalIssueReportValidator validator = new ExternalIssueReportValidator(documentationLinkGenerator);
56 public LogTester logTester = new LogTester();
60 when(documentationLinkGenerator.getDocumentationLink(URL)).thenReturn(TEST_URL);
64 public void validate_whenInvalidReport_shouldThrowException() throws IOException {
65 ExternalIssueReport report = readInvalidReport(DEPRECATED_REPORTS_LOCATION);
66 assertThatThrownBy(() -> validator.validate(report, reportPath))
67 .isInstanceOf(IllegalStateException.class)
68 .hasMessage("Failed to parse report 'report-path': invalid report detected.");
72 public void validate_whenCorrect_shouldNotThrowException() throws IOException {
73 ExternalIssueReport report = read(REPORTS_LOCATION);
74 assertThatCode(() -> validator.validate(report, reportPath)).doesNotThrowAnyException();
78 public void validate_whenDuplicateRuleIdFound_shouldThrowException() throws IOException {
79 ExternalIssueReport report = read(REPORTS_LOCATION);
80 report.rules[0].id = "rule2";
82 assertThatThrownBy(() -> validator.validate(report, reportPath))
83 .isInstanceOf(IllegalStateException.class)
84 .hasMessage("Failed to parse report 'report-path': found duplicate rule ID 'rule2'.");
88 public void validate_whenMissingMandatoryCleanCodeAttributeField_shouldThrowException() throws IOException {
89 ExternalIssueReport report = read(REPORTS_LOCATION);
90 report.rules[0].cleanCodeAttribute = null;
92 assertThatThrownBy(() -> validator.validate(report, reportPath))
93 .isInstanceOf(IllegalStateException.class)
94 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'cleanCodeAttribute'.");
98 public void validate_whenMissingEngineIdField_shouldThrowException() throws IOException {
99 ExternalIssueReport report = read(REPORTS_LOCATION);
100 report.rules[0].engineId = null;
102 assertThatThrownBy(() -> validator.validate(report, reportPath))
103 .isInstanceOf(IllegalStateException.class)
104 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'engineId'.");
108 public void validate_whenMissingFilepathFieldForPrimaryLocation_shouldThrowException() throws IOException {
109 ExternalIssueReport report = read(REPORTS_LOCATION);
110 report.issues[0].primaryLocation.filePath = null;
112 assertThatThrownBy(() -> validator.validate(report, reportPath))
113 .isInstanceOf(IllegalStateException.class)
114 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in the primary location of the issue.");
118 public void validate_whenMissingImpactsField_shouldThrowException() throws IOException {
119 ExternalIssueReport report = read(REPORTS_LOCATION);
120 report.rules[0].impacts = null;
122 assertThatThrownBy(() -> validator.validate(report, reportPath))
123 .isInstanceOf(IllegalStateException.class)
124 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'impacts'.");
128 public void validate_whenMissingMessageFieldForPrimaryLocation_shouldThrowException() throws IOException {
129 ExternalIssueReport report = read(REPORTS_LOCATION);
130 report.issues[0].primaryLocation.message = null;
132 assertThatThrownBy(() -> validator.validate(report, reportPath))
133 .isInstanceOf(IllegalStateException.class)
134 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'message' in the primary location of the issue.");
138 public void validate_whenMissingStartLineFieldForPrimaryLocation_shouldThrowException() throws IOException {
139 ExternalIssueReport report = read(REPORTS_LOCATION);
140 report.issues[0].primaryLocation.textRange.startLine = null;
142 assertThatThrownBy(() -> validator.validate(report, reportPath))
143 .isInstanceOf(IllegalStateException.class)
144 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
148 public void validate_whenReportMissingFilePathForSecondaryLocation_shouldThrowException() throws IOException {
149 ExternalIssueReport report = read(REPORTS_LOCATION);
150 report.issues[3].secondaryLocations[0].filePath = null;
152 assertThatThrownBy(() -> validator.validate(report, reportPath))
153 .isInstanceOf(IllegalStateException.class)
154 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in a secondary location of the issue.");
158 public void validate_whenReportMissingTextRangeForSecondaryLocation_shouldThrowException() throws IOException {
159 ExternalIssueReport report = read(REPORTS_LOCATION);
160 report.issues[3].secondaryLocations[0].textRange = null;
162 assertThatThrownBy(() -> validator.validate(report, reportPath))
163 .isInstanceOf(IllegalStateException.class)
164 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'textRange' in a secondary location of the issue.");
168 public void validate_whenReportMissingTextRangeStartLineForSecondaryLocation_shouldThrowException() throws IOException {
169 ExternalIssueReport report = read(REPORTS_LOCATION);
170 report.issues[3].secondaryLocations[0].textRange.startLine = null;
172 assertThatThrownBy(() -> validator.validate(report, reportPath))
173 .isInstanceOf(IllegalStateException.class)
174 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
178 public void validate_whenMissingNameField_shouldThrowException() throws IOException {
179 ExternalIssueReport report = read(REPORTS_LOCATION);
180 report.rules[0].name = null;
182 assertThatThrownBy(() -> validator.validate(report, reportPath))
183 .isInstanceOf(IllegalStateException.class)
184 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'name'.");
188 public void validate_whenMissingPrimaryLocationField_shouldThrowException() throws IOException {
189 ExternalIssueReport report = read(REPORTS_LOCATION);
190 report.issues[0].primaryLocation = null;
192 assertThatThrownBy(() -> validator.validate(report, reportPath))
193 .isInstanceOf(IllegalStateException.class)
194 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'primaryLocation'.");
198 public void validate_whenMissingOrEmptyRuleIdField_shouldThrowException() throws IOException {
199 String errorMessage = "Failed to parse report 'report-path': missing mandatory field 'id'.";
201 ExternalIssueReport report = read(REPORTS_LOCATION);
202 report.rules[0].id = null;
203 assertThatThrownBy(() -> validator.validate(report, reportPath))
204 .isInstanceOf(IllegalStateException.class)
205 .hasMessage(errorMessage);
207 report.rules[0].id = "";
208 assertThatThrownBy(() -> validator.validate(report, reportPath))
209 .isInstanceOf(IllegalStateException.class)
210 .hasMessage(errorMessage);
214 public void validate_whenIssueContainsRuleIdNotPresentInReport_shouldThrowException() throws IOException {
215 ExternalIssueReport report = read(REPORTS_LOCATION);
216 report.issues[0].ruleId = "rule-id-not-present";
218 assertThatThrownBy(() -> validator.validate(report, reportPath))
219 .isInstanceOf(IllegalStateException.class)
220 .hasMessage("Failed to parse report 'report-path': rule with 'rule-id-not-present' not present.");
224 public void validate_whenIssueRuleIdNotPresentInReport_shouldThrowException() throws IOException {
225 ExternalIssueReport report = read(REPORTS_LOCATION);
226 report.issues[0].ruleId = null;
228 assertThatThrownBy(() -> validator.validate(report, reportPath))
229 .isInstanceOf(IllegalStateException.class)
230 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'ruleId'.");
234 public void validate_whenContainsDeprecatedSeverityEntry_shouldThrowException() throws IOException {
235 ExternalIssueReport report = read(REPORTS_LOCATION);
236 report.issues[0].severity = "MAJOR";
238 assertThatThrownBy(() -> validator.validate(report, reportPath))
239 .isInstanceOf(IllegalStateException.class)
240 .hasMessage("Deprecated 'severity' field found in the following report: 'report-path'.");
244 public void validate_whenContainsDeprecatedTypeEntry_shouldThrowException() throws IOException {
245 ExternalIssueReport report = read(REPORTS_LOCATION);
246 report.issues[0].type = "BUG";
248 assertThatThrownBy(() -> validator.validate(report, reportPath))
249 .isInstanceOf(IllegalStateException.class)
250 .hasMessage("Deprecated 'type' field found in the following report: 'report-path'.");
254 public void validate_whenContainsEmptyImpacts_shouldThrowException() throws IOException {
255 ExternalIssueReport report = read(REPORTS_LOCATION);
256 report.rules[0].impacts = new ExternalIssueReport.Impact[0];
258 assertThatThrownBy(() -> validator.validate(report, reportPath))
259 .isInstanceOf(IllegalStateException.class)
260 .hasMessage("Failed to parse report 'report-path': mandatory array 'impacts' not populated.");
264 public void validate_whenDeprecatedReportFormat_shouldValidateWithWarningLog() throws IOException {
265 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
266 assertThatCode(() -> validator.validate(report, reportPath)).doesNotThrowAnyException();
271 public void validate_whenDeprecatedReportMissingEngineId_shouldThrowException() throws IOException {
272 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
273 report.issues[0].engineId = null;
275 assertThatThrownBy(() -> validator.validate(report, reportPath))
276 .isInstanceOf(IllegalStateException.class)
277 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'engineId'.");
282 public void validate_whenDeprecatedReportMissingFilepathForPrimaryLocation_shouldThrowException() throws IOException {
283 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
284 report.issues[0].primaryLocation.filePath = null;
286 assertThatThrownBy(() -> validator.validate(report, reportPath))
287 .isInstanceOf(IllegalStateException.class)
288 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in the primary location of the issue.");
293 public void validate_whenDeprecatedReportMissingMessageForPrimaryLocation_shouldThrowException() throws IOException {
294 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
295 report.issues[0].primaryLocation.message = null;
297 assertThatThrownBy(() -> validator.validate(report, reportPath))
298 .isInstanceOf(IllegalStateException.class)
299 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'message' in the primary location of the issue.");
304 public void validate_whenDeprecatedReportMissingPrimaryLocation_shouldThrowException() throws IOException {
305 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
306 report.issues[0].primaryLocation = null;
308 assertThatThrownBy(() -> validator.validate(report, reportPath))
309 .isInstanceOf(IllegalStateException.class)
310 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'primaryLocation'.");
315 public void validate_whenDeprecatedReportMissingStartLineForPrimaryLocation_shouldThrowException() throws IOException {
316 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
317 report.issues[0].primaryLocation.textRange.startLine = null;
319 assertThatThrownBy(() -> validator.validate(report, reportPath))
320 .isInstanceOf(IllegalStateException.class)
321 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in the primary location of the issue.");
326 public void validate_whenDeprecatedReportMissingFilePathForSecondaryLocation_shouldThrowException() throws IOException {
327 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
328 report.issues[3].secondaryLocations[0].filePath = null;
330 assertThatThrownBy(() -> validator.validate(report, reportPath))
331 .isInstanceOf(IllegalStateException.class)
332 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'filePath' in a secondary location of the issue.");
337 public void validate_whenDeprecatedReportMissingTextRangeForSecondaryLocation_shouldThrowException() throws IOException {
338 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
339 report.issues[3].secondaryLocations[0].textRange = null;
341 assertThatThrownBy(() -> validator.validate(report, reportPath))
342 .isInstanceOf(IllegalStateException.class)
343 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'textRange' in a secondary location of the issue.");
348 public void validate_whenDeprecatedReportMissingTextRangeStartLineForSecondaryLocation_shouldThrowException() throws IOException {
349 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
350 report.issues[3].secondaryLocations[0].textRange.startLine = null;
352 assertThatThrownBy(() -> validator.validate(report, reportPath))
353 .isInstanceOf(IllegalStateException.class)
354 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'startLine of the text range' in a secondary location of the issue.");
359 public void validate_whenDeprecatedReportMissingRuleId_shouldThrowException() throws IOException {
360 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
361 report.issues[0].ruleId = null;
363 assertThatThrownBy(() -> validator.validate(report, reportPath))
364 .isInstanceOf(IllegalStateException.class)
365 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'ruleId'.");
370 public void validate_whenDeprecatedReportMissingSeverity_shouldThrowException() throws IOException {
371 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
372 report.issues[0].severity = null;
374 assertThatThrownBy(() -> validator.validate(report, reportPath))
375 .isInstanceOf(IllegalStateException.class)
376 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'severity'.");
381 public void validate_whenDeprecatedReportMissingType_shouldThrowException() throws IOException {
382 ExternalIssueReport report = read(DEPRECATED_REPORTS_LOCATION);
383 report.issues[0].type = null;
385 assertThatThrownBy(() -> validator.validate(report, reportPath))
386 .isInstanceOf(IllegalStateException.class)
387 .hasMessage("Failed to parse report 'report-path': missing mandatory field 'type'.");
391 private void assertWarningLog() {
392 assertThat(logTester.getLogs(Level.WARN))
393 .extracting(LogAndArguments::getFormattedMsg)
394 .contains("External issues were imported with a deprecated format which will be removed soon. " +
395 "Please switch to the newest format to fully benefit from Clean Code: " + TEST_URL);
398 private ExternalIssueReport read(String location) throws IOException {
399 Reader reader = Files.newBufferedReader(Paths.get(location + "report.json"), StandardCharsets.UTF_8);
400 return gson.fromJson(reader, ExternalIssueReport.class);
403 private ExternalIssueReport readInvalidReport(String location) throws IOException {
404 Reader reader = Files.newBufferedReader(Paths.get(location + "invalid_report.json"), StandardCharsets.UTF_8);
405 return gson.fromJson(reader, ExternalIssueReport.class);