diff options
79 files changed, 1244 insertions, 1610 deletions
diff --git a/build.gradle b/build.gradle index 18b5aa25637..e1936ee2a03 100644 --- a/build.gradle +++ b/build.gradle @@ -283,15 +283,15 @@ subprojects { // bundled plugin list -- keep it alphabetically ordered dependency 'com.sonarsource.abap:sonar-abap-plugin:3.15.1.6010' dependency 'com.sonarsource.cobol:sonar-cobol-plugin:5.8.1.8428' - dependency 'com.sonarsource.cpp:sonar-cfamily-dependencies-plugin:6.64.1.81116' - dependency 'com.sonarsource.cpp:sonar-cfamily-plugin:6.64.1.81116' - dependency 'com.sonarsource.dart:sonar-dart-plugin:1.0.0.1952' + dependency 'com.sonarsource.cpp:sonar-cfamily-dependencies-plugin:6.65.0.81949' + dependency 'com.sonarsource.cpp:sonar-cfamily-plugin:6.65.0.81949' + dependency 'com.sonarsource.dart:sonar-dart-plugin:1.1.0.2133' dependency 'com.sonarsource.dbd:sonar-dbd-plugin:1.36.1.13250' dependency 'com.sonarsource.dbd:sonar-dbd-java-frontend-plugin:1.36.1.13250' dependency 'com.sonarsource.dbd:sonar-dbd-python-frontend-plugin:1.36.1.13250' dependency 'com.sonarsource.dotnet:sonar-csharp-enterprise-plugin:10.7.0.110445' dependency 'com.sonarsource.dotnet:sonar-vbnet-enterprise-plugin:10.7.0.110445' - dependency 'com.sonarsource.go:sonar-go-enterprise-plugin:1.20.0.1242' + dependency 'com.sonarsource.go:sonar-go-enterprise-plugin:1.21.0.1607' dependency 'com.sonarsource.pli:sonar-pli-plugin:1.16.0.5325' dependency 'com.sonarsource.plsql:sonar-plsql-plugin:3.15.0.7123' dependency 'com.sonarsource.plugins.vb:sonar-vb-plugin:2.14.0.5475' @@ -310,17 +310,17 @@ subprojects { dependency 'org.sonarsource.dotnet:sonar-csharp-plugin:10.7.0.110445' dependency 'org.sonarsource.dotnet:sonar-vbnet-plugin:10.7.0.110445' dependency 'org.sonarsource.flex:sonar-flex-plugin:2.14.0.5032' - dependency 'org.sonarsource.go:sonar-go-plugin:1.20.0.1242' + dependency 'org.sonarsource.go:sonar-go-plugin:1.21.0.1607' dependency 'org.sonarsource.html:sonar-html-plugin:3.19.0.5695' dependency 'org.sonarsource.jacoco:sonar-jacoco-plugin:1.3.0.1538' - dependency 'org.sonarsource.java:sonar-java-plugin:8.10.0.38194' - dependency 'org.sonarsource.java:sonar-java-symbolic-execution-plugin:8.10.0.38194' + dependency 'org.sonarsource.java:sonar-java-plugin:8.11.0.38440' + dependency 'org.sonarsource.java:sonar-java-symbolic-execution-plugin:8.11.0.38440' dependency 'org.sonarsource.javascript:sonar-javascript-plugin:10.21.1.30825' dependency 'org.sonarsource.php:sonar-php-plugin:3.45.0.12991' dependency 'org.sonarsource.plugins.cayc:sonar-cayc-plugin:2.4.0.2018' - dependency 'org.sonarsource.python:sonar-python-plugin:5.1.0.20567' - dependency 'com.sonarsource.python:sonar-python-enterprise-plugin:5.1.0.20567' - dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:2.22.0.5972' + dependency 'org.sonarsource.python:sonar-python-plugin:5.2.0.20808' + dependency 'com.sonarsource.python:sonar-python-enterprise-plugin:5.2.0.20808' + dependency 'org.sonarsource.kotlin:sonar-kotlin-plugin:3.0.1.6889' dependency "org.sonarsource.api.plugin:sonar-plugin-api:$pluginApiVersion" dependency "org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures:$pluginApiVersion" dependency 'org.sonarsource.xml:sonar-xml-plugin:2.12.0.5749' @@ -330,9 +330,9 @@ subprojects { dependency 'com.sonarsource.text:sonar-text-developer-plugin:2.21.1.5779' dependency 'com.sonarsource.text:sonar-text-enterprise-plugin:2.21.1.5779' dependency 'com.sonarsource.jcl:sonar-jcl-plugin:1.4.1.1493' - dependency 'com.sonarsource.architecture:sonar-architecture-plugin:1.8.0.4005' - dependency 'com.sonarsource.architecture:sonar-architecture-java-frontend-plugin:1.8.0.4005' - dependency 'com.sonarsource.architecture:sonar-architecture-javascript-frontend-plugin:1.8.0.4005' + dependency 'com.sonarsource.architecture:sonar-architecture-plugin:1.9.0.4841' + dependency 'com.sonarsource.architecture:sonar-architecture-java-frontend-plugin:1.9.0.4841' + dependency 'com.sonarsource.architecture:sonar-architecture-javascript-frontend-plugin:1.9.0.4841' // Webapp dependency "org.sonarsource.sonarqube:webapp-assets:$webappVersion" @@ -511,10 +511,10 @@ subprojects { dependency 'org.reflections:reflections:0.10.2' dependency 'org.simpleframework:simple:5.1.6' dependency 'org.sonarsource.git.blame:git-files-blame:1.1.0.1835' - dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit4:5.2.0.2403') { + dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit4:5.4.0.2489') { exclude 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' } - dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit5:5.2.0.2403') { + dependency('org.sonarsource.orchestrator:sonar-orchestrator-junit5:5.4.0.2489') { exclude 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' } dependency 'com.sonarsource.pdfreport:security-report-pdf-generation:2.0.0.184' diff --git a/gradle.properties b/gradle.properties index b5075b26036..14e9302ede0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,4 +15,4 @@ elasticSearchServerVersion=8.16.3 projectType=application artifactoryUrl=https://repox.jfrog.io/repox jre_release_name=jdk-17.0.13+11 -webappVersion=2025.2.0.14025 +webappVersion=2025.2.0.14519 diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java index b32bca06bda..b4aad85bdb0 100644 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java +++ b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/XooPlugin.java @@ -78,9 +78,6 @@ import org.sonar.xoo.rule.Xoo2SonarWayProfile; import org.sonar.xoo.rule.XooBasicProfile; import org.sonar.xoo.rule.XooBuiltInQualityProfilesDefinition; import org.sonar.xoo.rule.XooEmptyProfile; -import org.sonar.xoo.rule.XooFakeExporter; -import org.sonar.xoo.rule.XooFakeImporter; -import org.sonar.xoo.rule.XooFakeImporterWithMessages; import org.sonar.xoo.rule.XooRulesDefinition; import org.sonar.xoo.rule.XooSonarWayProfile; import org.sonar.xoo.rule.hotspot.HotspotWithContextsSensor; @@ -138,10 +135,6 @@ public class XooPlugin implements Plugin { Xoo2BasicProfile.class, XooEmptyProfile.class, - XooFakeExporter.class, - XooFakeImporter.class, - XooFakeImporterWithMessages.class, - // SCM XooScmProvider.class, XooBlameCommand.class, diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeExporter.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeExporter.java deleted file mode 100644 index 69a6068731e..00000000000 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeExporter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.xoo.rule; - -import org.sonar.api.profiles.ProfileExporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.xoo.Xoo; - -import java.io.IOException; -import java.io.Writer; - -/** - * Fake exporter just for test - */ -public class XooFakeExporter extends ProfileExporter { - public XooFakeExporter() { - super("XooFakeExporter", "Xoo Fake Exporter"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[]{Xoo.KEY}; - } - - @Override - public String getMimeType() { - return "plain/custom"; - } - - @Override - public void exportProfile(RulesProfile profile, Writer writer) { - try { - writer.write("xoo -> " + profile.getName() + " -> " + profile.getActiveRules().size()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } -} diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporter.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporter.java deleted file mode 100644 index a89374e504b..00000000000 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporter.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.xoo.rule; - -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.rules.Rule; -import org.sonar.api.rules.RulePriority; -import org.sonar.api.utils.ValidationMessages; -import org.sonar.xoo.Xoo; - -import java.io.Reader; - -/** - * Fake importer just for test, it will NOT take into account the given file but will create some hard-coded rules - */ -public class XooFakeImporter extends ProfileImporter { - public XooFakeImporter() { - super("XooProfileImporter", "Xoo Profile Importer"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[] {Xoo.KEY}; - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - RulesProfile rulesProfile = RulesProfile.create(); - rulesProfile.activateRule(Rule.create(XooRulesDefinition.XOO_REPOSITORY, "x1"), RulePriority.CRITICAL); - return rulesProfile; - } -} diff --git a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporterWithMessages.java b/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporterWithMessages.java deleted file mode 100644 index e7f31ec0ec1..00000000000 --- a/plugins/sonar-xoo-plugin/src/main/java/org/sonar/xoo/rule/XooFakeImporterWithMessages.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.xoo.rule; - -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.utils.ValidationMessages; - -import java.io.Reader; - -/** - * Fake importer just for test, it will NOT take into account the given file but will display some info and warning messages - */ -public class XooFakeImporterWithMessages extends ProfileImporter { - public XooFakeImporterWithMessages() { - super("XooFakeImporterWithMessages", "Xoo Profile Importer With Messages"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[] {}; - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - messages.addWarningText("a warning"); - messages.addInfoText("an info"); - return RulesProfile.create(); - } -} diff --git a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java index 10ec76b6d49..737e076950e 100644 --- a/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java +++ b/server/sonar-db-core/src/testFixtures/java/org/sonar/db/AbstractDbTester.java @@ -83,7 +83,8 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { private static final Map<Integer, Integer> POSTGRES_TYPE_SUBSTITUTION = Map.of( BOOLEAN, BIT, DOUBLE, NUMERIC, - CLOB, VARCHAR); + CLOB, VARCHAR, + DECIMAL, NUMERIC); private static final Map<Integer, Integer> MSSQL_TYPE_SUBSTITUTION = Map.of( BOOLEAN, BIT, VARCHAR, NVARCHAR, @@ -93,7 +94,8 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { BOOLEAN, NUMERIC, BIGINT, NUMERIC, INTEGER, NUMERIC, - DOUBLE, NUMERIC); + DOUBLE, NUMERIC, + DECIMAL, NUMERIC); protected final T db; @@ -125,7 +127,7 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { public void executeDdl(String ddl) { try (Connection connection = getConnection(); - Statement stmt = connection.createStatement()) { + Statement stmt = connection.createStatement()) { stmt.execute(ddl); } catch (SQLException e) { throw new IllegalStateException("Failed to execute DDL: " + ddl, e); @@ -164,10 +166,10 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { } String sql = "insert into " + table.toLowerCase(Locale.ENGLISH) + " (" + - COMMA_JOINER.join(valuesByColumn.keySet().stream().map(t -> t.toLowerCase(Locale.ENGLISH)).toArray(String[]::new)) + - ") values (" + - COMMA_JOINER.join(Collections.nCopies(valuesByColumn.size(), '?')) + - ")"; + COMMA_JOINER.join(valuesByColumn.keySet().stream().map(t -> t.toLowerCase(Locale.ENGLISH)).toArray(String[]::new)) + + ") values (" + + COMMA_JOINER.join(Collections.nCopies(valuesByColumn.size(), '?')) + + ")"; executeUpdateSql(sql, valuesByColumn.values().toArray(new Object[valuesByColumn.size()])); } @@ -290,7 +292,7 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { public void assertColumnDefinition(String table, String column, int expectedType, @Nullable Integer expectedSize, @Nullable Boolean isNullable) { try (Connection connection = getConnection(); - ResultSet rs = connection.getMetaData().getColumns(null, null, toVendorCase(table), toVendorCase(column))) { + ResultSet rs = connection.getMetaData().getColumns(null, null, toVendorCase(table), toVendorCase(column))) { boolean exists = false; while (rs.next()) { @@ -326,8 +328,8 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { public void assertColumnDoesNotExist(String table, String column) throws SQLException { try (Connection connection = getConnection(); - PreparedStatement stmt = connection.prepareStatement("select * from " + table); - ResultSet res = stmt.executeQuery()) { + PreparedStatement stmt = connection.prepareStatement("select * from " + table); + ResultSet res = stmt.executeQuery()) { assertThat(getColumnNames(res)).doesNotContain(column); } } @@ -365,7 +367,7 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { private void assertIndexImpl(String tableName, String indexName, boolean expectedUnique, String expectedColumn, String... expectedSecondaryColumns) { try (Connection connection = getConnection(); - ResultSet rs = connection.getMetaData().getIndexInfo(null, null, toVendorCase(tableName), false, false)) { + ResultSet rs = connection.getMetaData().getIndexInfo(null, null, toVendorCase(tableName), false, false)) { List<String> onColumns = new ArrayList<>(); while (rs.next()) { @@ -403,7 +405,7 @@ public class AbstractDbTester<T extends TestDb> extends ExternalResource { */ public void assertIndexDoesNotExist(String tableName, String indexName) { try (Connection connection = getConnection(); - ResultSet rs = connection.getMetaData().getIndexInfo(null, null, tableName.toUpperCase(Locale.ENGLISH), false, false)) { + ResultSet rs = connection.getMetaData().getIndexInfo(null, null, tableName.toUpperCase(Locale.ENGLISH), false, false)) { List<String> indices = new ArrayList<>(); while (rs.next()) { if (rs.getString("INDEX_NAME") != null) { diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/property/PropertiesDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/property/PropertiesDaoIT.java index 8cc5c95e032..05283591dbe 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/property/PropertiesDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/property/PropertiesDaoIT.java @@ -30,6 +30,7 @@ import java.util.Set; import java.util.function.Consumer; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.assertj.core.groups.Tuple; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -125,10 +126,10 @@ class PropertiesDaoIT { // Global + Project subscribers assertThat(underTest.hasProjectNotificationSubscribersForDispatchers(projectUuid, singletonList( "DispatcherWithGlobalAndProjectSubscribers"))) - .isTrue(); + .isTrue(); assertThat(underTest.hasProjectNotificationSubscribersForDispatchers("PROJECT_B", singletonList( "DispatcherWithGlobalAndProjectSubscribers"))) - .isTrue(); + .isTrue(); } @Test @@ -536,7 +537,7 @@ class PropertiesDaoIT { } private static Object[][] allValuesForSelect() { - return new Object[][] { + return new Object[][]{ {null, ""}, {"", ""}, {"some value", "some value"}, @@ -561,6 +562,26 @@ class PropertiesDaoIT { } @Test + void selectUserProperty() { + final String propertyKey = "user.property.one"; + + UserDto userDto1 = db.users().insertUser(); + UserDto userDto2 = db.users().insertUser(); + + insertProperty(propertyKey, "one", null, userDto1.getUuid(), null, null, null); + insertProperty(propertyKey, "two", null, userDto2.getUuid(), null, null, null); + + List<PropertyDto> property = underTest.selectUserPropertiesByKey(db.getSession(), propertyKey); + + assertThat(property) + .extracting(PropertyDto::getKey, PropertyDto::getEntityUuid, PropertyDto::getUserUuid, PropertyDto::getValue) + .containsExactlyInAnyOrderElementsOf(Set.of( + Tuple.tuple(propertyKey, null, userDto1.getUuid(), "one"), + Tuple.tuple(propertyKey, null, userDto2.getUuid(), "two") + )); + } + + @Test void select_by_query() { // global insertProperty("global.one", "one", null, null, null, null, null); @@ -641,10 +662,10 @@ class PropertiesDaoIT { tuple(key, project2.getUuid())); assertThat(underTest.selectPropertiesByKeysAndEntityUuids(session, newHashSet(key, anotherKey), newHashSet(project.getUuid(), project2.getUuid()))) - .extracting(PropertyDto::getKey, PropertyDto::getEntityUuid).containsOnly( - tuple(key, project.getUuid()), - tuple(key, project2.getUuid()), - tuple(anotherKey, project2.getUuid())); + .extracting(PropertyDto::getKey, PropertyDto::getEntityUuid).containsOnly( + tuple(key, project.getUuid()), + tuple(key, project2.getUuid()), + tuple(anotherKey, project2.getUuid())); assertThat(underTest.selectPropertiesByKeysAndEntityUuids(session, newHashSet("unknown"), newHashSet(project.getUuid()))).isEmpty(); assertThat(underTest.selectPropertiesByKeysAndEntityUuids(session, newHashSet("key"), newHashSet("uuid123456789"))).isEmpty(); @@ -849,7 +870,7 @@ class PropertiesDaoIT { } static Object[][] valueUpdatesDataProvider() { - return new Object[][] { + return new Object[][]{ {null, null}, {null, ""}, {null, "some value"}, @@ -934,8 +955,8 @@ class PropertiesDaoIT { assertThat(db.select("select prop_key as \"key\", text_value as \"value\", entity_uuid as \"projectUuid\", user_uuid as \"userUuid\" " + "from properties")) - .extracting((row) -> row.get("key"), (row) -> row.get("value"), (row) -> row.get("projectUuid"), (row) -> row.get("userUuid")) - .containsOnly(tuple("KEY", "ANOTHER_VALUE", null, null), tuple("ANOTHER_KEY", "VALUE", project.uuid(), "100")); + .extracting((row) -> row.get("key"), (row) -> row.get("value"), (row) -> row.get("projectUuid"), (row) -> row.get("userUuid")) + .containsOnly(tuple("KEY", "ANOTHER_VALUE", null, null), tuple("ANOTHER_KEY", "VALUE", project.uuid(), "100")); } private static Map<String, String> mapOf(String... values) { diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java index c5bd44b7414..99c028718ef 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaDependenciesDaoIT.java @@ -41,7 +41,6 @@ class ScaDependenciesDaoIT { @Test void insert_shouldPersistScaDependencies() { - ComponentDto componentDto = prepareComponentDto(); ScaDependencyDto scaDependencyDto = db.getScaDependenciesDbTester().insertScaDependency("scaReleaseUuid", "1"); List<Map<String, Object>> select = db.select(db.getSession(), "select * from sca_dependencies"); @@ -64,7 +63,6 @@ class ScaDependenciesDaoIT { @Test void deleteByUuid_shouldDeleteScaDependencies() { - ComponentDto componentDto = prepareComponentDto(); ScaDependencyDto scaDependencyDto = db.getScaDependenciesDbTester().insertScaDependency("scaReleaseUuid", "1"); List<Map<String, Object>> select = db.select(db.getSession(), "select * from sca_dependencies"); @@ -78,7 +76,6 @@ class ScaDependenciesDaoIT { @Test void selectByUuid_shouldLoadScaDependency() { - ComponentDto componentDto = prepareComponentDto(); ScaDependencyDto scaDependencyDto = db.getScaDependenciesDbTester().insertScaDependency("scaReleaseUuid", "1"); var loadedOptional = scaDependenciesDao.selectByUuid(db.getSession(), scaDependencyDto.uuid()); @@ -202,7 +199,6 @@ class ScaDependenciesDaoIT { @Test void update_shouldUpdateScaDependency() { - ComponentDto componentDto = prepareComponentDto(); ScaDependencyDto scaDependencyDto = db.getScaDependenciesDbTester().insertScaDependency("scaReleaseUuid", "1", true); ScaDependencyDto updatedScaDependency = scaDependencyDto.toBuilder() .setUpdatedAt(scaDependencyDto.updatedAt() + 1) diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaIssuesReleasesDetailsDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaIssuesReleasesDetailsDaoIT.java index c023fbce2f1..4db2590aed3 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaIssuesReleasesDetailsDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/sca/ScaIssuesReleasesDetailsDaoIT.java @@ -53,6 +53,35 @@ class ScaIssuesReleasesDetailsDaoIT { .thenComparing(ScaIssueReleaseDetailsDto::issueReleaseUuid); } + private static Comparator<ScaIssueReleaseDetailsDto> severityComparator() { + return Comparator.comparing(dto -> dto.severity().databaseSortKey()); + } + + private static Comparator<ScaIssueReleaseDetailsDto> cvssScoreComparator() { + return Comparator.comparing(ScaIssueReleaseDetailsDto::cvssScore, + // we treat null cvss as a score of 0.0 + Comparator.nullsFirst(Comparator.naturalOrder())); + } + + private static Comparator<ScaIssueReleaseDetailsDto> comparator(ScaIssuesReleasesDetailsQuery.Sort sort) { + return switch (sort) { + case IDENTITY_ASC -> identityComparator(); + case IDENTITY_DESC -> identityComparator().reversed(); + case SEVERITY_ASC -> severityComparator() + .thenComparing(cvssScoreComparator()) + .thenComparing(identityComparator()); + case SEVERITY_DESC -> severityComparator().reversed() + .thenComparing(cvssScoreComparator().reversed()) + .thenComparing(identityComparator()); + case CVSS_SCORE_ASC -> cvssScoreComparator() + .thenComparing(ScaIssueReleaseDetailsDto::severity) + .thenComparing(identityComparator()); + case CVSS_SCORE_DESC -> cvssScoreComparator().reversed() + .thenComparing(Comparator.comparing(ScaIssueReleaseDetailsDto::severity).reversed()) + .thenComparing(identityComparator()); + }; + } + @Test void selectByBranchUuid_shouldReturnIssues() { var projectData = db.components().insertPrivateProject(); @@ -64,7 +93,7 @@ class ScaIssuesReleasesDetailsDaoIT { assertThat(foundPage).hasSize(1).isSubsetOf(issue1, issue2); var foundAllIssues = scaIssuesReleasesDetailsDao.selectByBranchUuid(db.getSession(), componentDto.branchUuid(), Pagination.forPage(1).andSize(10)); - assertThat(foundAllIssues).hasSize(2).containsExactlyElementsOf(Stream.of(issue1, issue2).sorted(identityComparator()).toList()); + assertThat(foundAllIssues).hasSize(2).containsExactlyElementsOf(Stream.of(issue1, issue2).sorted(comparator(ScaIssuesReleasesDetailsQuery.Sort.SEVERITY_DESC)).toList()); } @Test @@ -81,6 +110,34 @@ class ScaIssuesReleasesDetailsDaoIT { } @Test + void selectByReleaseUuid_shouldReturnIssues() { + var projectData = db.components().insertPrivateProject(); + var componentDto = projectData.getMainBranchComponent(); + var issue1 = db.getScaIssuesReleasesDetailsDbTester().insertIssue(ScaIssueType.VULNERABILITY, "1", componentDto.uuid()); + var release1 = issue1.releaseDto(); + // make these other issues use the same release and have a variety of CVSS + var issue2 = db.getScaIssuesReleasesDetailsDbTester().insertIssue(ScaIssueType.VULNERABILITY, "2", componentDto.uuid(), + null, vi -> vi.toBuilder().setCvssScore(new BigDecimal("1.1")).build(), + releaseDto -> release1, + issueReleaseDto -> issueReleaseDto.toBuilder().setScaReleaseUuid(release1.uuid()).build()); + var issue3 = db.getScaIssuesReleasesDetailsDbTester().insertIssue(ScaIssueType.VULNERABILITY, "3", componentDto.uuid(), + null, vi -> vi.toBuilder().setCvssScore(new BigDecimal("9.9")).build(), + releaseDto -> release1, + issueReleaseDto -> issueReleaseDto.toBuilder().setScaReleaseUuid(release1.uuid()).build()); + var issue4 = db.getScaIssuesReleasesDetailsDbTester().insertIssue(ScaIssueType.PROHIBITED_LICENSE, "4", componentDto.uuid(), + null, null, + releaseDto -> release1, + issueReleaseDto -> issueReleaseDto.toBuilder().setScaReleaseUuid(release1.uuid()).build()); + + var foundPage = scaIssuesReleasesDetailsDao.selectByBranchUuid(db.getSession(), componentDto.branchUuid(), Pagination.forPage(1).andSize(1)); + + assertThat(foundPage).hasSize(1).isSubsetOf(issue1, issue2, issue3, issue4); + var foundAllIssues = scaIssuesReleasesDetailsDao.selectByBranchUuid(db.getSession(), componentDto.branchUuid(), Pagination.forPage(1).andSize(10)); + assertThat(foundAllIssues).hasSize(4) + .containsExactlyElementsOf(Stream.of(issue1, issue2, issue3, issue4).sorted(comparator(ScaIssuesReleasesDetailsQuery.Sort.SEVERITY_DESC)).toList()); + } + + @Test void withNoQueryFilters_shouldReturnAllIssues() { setupAndExecuteQueryTest(Function.identity(), QueryTestData::expectedIssuesSortedByIdentityAsc, "All issues should be returned"); } @@ -531,61 +588,21 @@ class ScaIssuesReleasesDetailsDaoIT { List<ScaIssueReleaseDetailsDto> transitiveIssues, List<ScaIssueReleaseDetailsDto> productionIssues, List<ScaIssueReleaseDetailsDto> notProductionIssues) { - private static Comparator<ScaIssueReleaseDetailsDto> cvssScoreComparator() { - return Comparator.comparing(ScaIssueReleaseDetailsDto::cvssScore, - // we treat null cvss as a score of 0.0 - Comparator.nullsFirst(Comparator.naturalOrder())); - } - - private static Comparator<ScaIssueReleaseDetailsDto> severityComparator() { - return Comparator.comparing(dto -> dto.severity().databaseSortKey()); - } public String branchUuid() { return componentDto.branchUuid(); } - public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedByIdentityAsc() { - return expectedIssues.stream().sorted(identityComparator()).toList(); - } - - public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedByIdentityDesc() { - return expectedIssues.stream().sorted(identityComparator().reversed()).toList(); - } - - public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedBySeverityAsc() { - return expectedIssues.stream().sorted(severityComparator() - .thenComparing(cvssScoreComparator()) - .thenComparing(identityComparator())).toList(); - } - - public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedBySeverityDesc() { - return expectedIssues.stream().sorted(severityComparator().reversed() - .thenComparing(cvssScoreComparator().reversed()) - .thenComparing(identityComparator())).toList(); + public List<ScaIssueReleaseDetailsDto> expectedIssuesSorted(ScaIssuesReleasesDetailsQuery.Sort sort) { + return expectedIssues.stream().sorted(comparator(sort)).toList(); } - public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedByCvssAsc() { - return expectedIssues.stream().sorted(cvssScoreComparator() - .thenComparing(ScaIssueReleaseDetailsDto::severity) - .thenComparing(identityComparator())).toList(); + public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedByIdentityAsc() { + return expectedIssuesSorted(ScaIssuesReleasesDetailsQuery.Sort.IDENTITY_ASC); } public List<ScaIssueReleaseDetailsDto> expectedIssuesSortedByCvssDesc() { - return expectedIssues.stream().sorted(cvssScoreComparator().reversed() - .thenComparing(Comparator.comparing(ScaIssueReleaseDetailsDto::severity).reversed()) - .thenComparing(identityComparator())).toList(); - } - - public List<ScaIssueReleaseDetailsDto> expectedIssuesSorted(ScaIssuesReleasesDetailsQuery.Sort sort) { - return switch (sort) { - case IDENTITY_ASC -> expectedIssuesSortedByIdentityAsc(); - case IDENTITY_DESC -> expectedIssuesSortedByIdentityDesc(); - case SEVERITY_ASC -> expectedIssuesSortedBySeverityAsc(); - case SEVERITY_DESC -> expectedIssuesSortedBySeverityDesc(); - case CVSS_SCORE_ASC -> expectedIssuesSortedByCvssAsc(); - case CVSS_SCORE_DESC -> expectedIssuesSortedByCvssDesc(); - }; + return expectedIssuesSorted(ScaIssuesReleasesDetailsQuery.Sort.CVSS_SCORE_DESC); } public List<ScaIssueReleaseDetailsDto> expectedIssuesWithPackageManager(PackageManager packageManager) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java index 97d5ee872e4..824ea54b14e 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesDao.java @@ -179,6 +179,10 @@ public class PropertiesDao implements Dao { return getMapper(session).selectProjectPropertyByKey(key); } + public List<PropertyDto> selectUserPropertiesByKey(DbSession session, String key) { + return getMapper(session).selectUserPropertiesByKey(key); + } + /** * Saves the specified property and its value. * <p> diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java index c1685ad9a59..8fb96f382a2 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/property/PropertiesMapper.java @@ -42,6 +42,8 @@ public interface PropertiesMapper { List<PropertyDto> selectProjectPropertyByKey(@Param("key") String key); + List<PropertyDto> selectUserPropertiesByKey(@Param("key") String key); + List<PropertyDto> selectByEntityUuids(@Param("entityUuids") List<String> entityUuids); List<PropertyDto> selectByQuery(@Param("query") PropertyQuery query); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java index 7187f8d4c2c..0c4b7798934 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/qualityprofile/ActiveRuleDto.java @@ -31,7 +31,6 @@ import org.apache.commons.lang3.builder.ToStringStyle; import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; -import org.sonar.api.rules.ActiveRule; import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.SeverityUtil; @@ -39,8 +38,8 @@ import static java.util.Objects.requireNonNull; public class ActiveRuleDto { - public static final String INHERITED = ActiveRule.INHERITED; - public static final String OVERRIDES = ActiveRule.OVERRIDES; + public static final String INHERITED = "INHERITED"; + public static final String OVERRIDES = "OVERRIDES"; private static final Gson GSON = new Gson(); private static final Type TYPE = new TypeToken<Map<SoftwareQuality, Severity>>() { }.getType(); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml index b186e885986..a6c5ca9e2e0 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/property/PropertiesMapper.xml @@ -163,6 +163,16 @@ and p.user_uuid is null </select> + <select id="selectUserPropertiesByKey" parameterType="map" resultType="ScrapProperty"> + select + <include refid="columnsToScrapPropertyDto"/> + from properties p + inner join users u on u.uuid=p.user_uuid + where + p.prop_key = #{key, jdbcType=VARCHAR} + and p.entity_uuid is null + </select> + <select id="selectByQuery" parameterType="map" resultType="ScrapProperty"> select <include refid="columnsToScrapPropertyDto"/> diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaIssuesReleasesDetailsMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaIssuesReleasesDetailsMapper.xml index 8f621db36b2..fc5b3468983 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaIssuesReleasesDetailsMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/sca/ScaIssuesReleasesDetailsMapper.xml @@ -19,7 +19,7 @@ </constructor> </resultMap> - <sql id="issuesWithScaColumns"> + <sql id="columns"> <!-- These have to match all of the properties in the other tables' mappers, adding the columnPrefix given in our resultMap above --> sir.uuid as issue_release_uuid, @@ -57,6 +57,17 @@ svi.updated_at as svi_updated_at </sql> + <sql id="columnsWithCvssSortKey"> + <include refid="columns"/>, + <!-- It seems that the behavior of NULL in ORDER BY varies by database backend, with different + defaults and a lack of universal support for NULLS FIRST / NULLS LAST. + This poses an issue for nullable columns we want to sort by such as cvss_score. + On databases that support it, NULLS FIRST could probably use the index while this COALESCE + hack does not, so maybe someday we want to conditionalize on db backend somehow. --> + <!-- NULL score is treated as least severe --> + COALESCE(svi.cvss_score, 0.0) as cvss_sort_key + </sql> + <sql id="sqlBaseJoins"> from sca_issues_releases sir inner join sca_issues si on sir.sca_issue_uuid = si.uuid @@ -80,21 +91,22 @@ </sql> <select id="selectByReleaseUuid" parameterType="map" resultMap="scaIssueReleaseDetailsResultMap"> - select <include refid="issuesWithScaColumns"/> + select <include refid="columnsWithCvssSortKey"/> <include refid="sqlSelectByReleaseUuid"/> - ORDER BY <include refid="sqlIdentityOrderColumns"/> + <include refid="sqlOrderBySeverityDesc"/> </select> <select id="selectByBranchUuid" parameterType="map" resultMap="scaIssueReleaseDetailsResultMap"> - select <include refid="issuesWithScaColumns"/> + select <include refid="columnsWithCvssSortKey"/> <include refid="sqlSelectByBranchUuid"/> - ORDER BY <include refid="sqlIdentityOrderColumns"/> + <include refid="sqlOrderBySeverityDesc"/> <include refid="org.sonar.db.common.Common.pagination"/> </select> <select id="selectByScaIssueReleaseUuid" parameterType="map" resultMap="scaIssueReleaseDetailsResultMap"> - select <include refid="issuesWithScaColumns"/> + select <include refid="columns"/> <include refid="sqlSelectByScaIssueReleaseUuid"/> + <!-- no ORDER BY here because it's always one result --> </select> <select id="countByBranchUuid" parameterType="string" resultType="int"> @@ -166,10 +178,17 @@ <sql id="sqlIdentityOrderColumns"> <!-- the unique index is ordered as: scaIssueType, vulnerabilityId, packageUrl, spdxLicenseId so we're guessing (or hoping?) that is the most efficient sort order, and it should sort of make - more sense to users than random --> + more sense to users than random. This sort is alphabetical first by issue type then + by CVE ID or license name. --> si.sca_issue_type ASC, si.vulnerability_id ASC, si.package_url ASC, si.spdx_license_id ASC, sir.uuid ASC </sql> + <!-- this is the default sort for the selects that don't have a sort parameter (i.e. not the query) + but is probably slower than the identity sort until/unless we create a matching index --> + <sql id="sqlOrderBySeverityDesc"> + ORDER BY sir.severity_sort_key DESC, cvss_sort_key DESC, <include refid="sqlIdentityOrderColumns"/> + </sql> + <sql id="sqlOrderByQuery"> <choose> <when test="query.sort == @org.sonar.db.sca.ScaIssuesReleasesDetailsQuery$Sort@IDENTITY_ASC"> @@ -185,7 +204,7 @@ </when> <when test="query.sort == @org.sonar.db.sca.ScaIssuesReleasesDetailsQuery$Sort@SEVERITY_DESC"> <!-- because many severities are the same, we try to keep the user intent by ordering by cvss score secondarily --> - ORDER BY sir.severity_sort_key DESC, cvss_sort_key DESC, <include refid="sqlIdentityOrderColumns"/> + <include refid="sqlOrderBySeverityDesc"/> </when> <when test="query.sort == @org.sonar.db.sca.ScaIssuesReleasesDetailsQuery$Sort@CVSS_SCORE_ASC"> <!-- because cvss score can be null, we try to keep the user intent by ordering by severity secondarily --> @@ -203,14 +222,7 @@ </sql> <select id="selectByQuery" parameterType="map" resultMap="scaIssueReleaseDetailsResultMap"> - select <include refid="issuesWithScaColumns"/>, - <!-- It seems that the behavior of NULL in ORDER BY varies by database backend, with different - defaults and a lack of universal support for NULLS FIRST / NULLS LAST. - This poses an issue for nullable columns we want to sort by such as cvss_score. - On databases that support it, NULLS FIRST could probably use the index while this COALESCE - hack does not, so maybe someday we want to conditionalize on db backend somehow. --> - <!-- NULL score is treated as least severe --> - COALESCE(svi.cvss_score, 0.0) as cvss_sort_key + select <include refid="columnsWithCvssSortKey"/> <include refid="sqlBaseJoins"/> <include refid="sqlSelectByQueryWhereClause"/> <include refid="sqlOrderByQuery"/> diff --git a/server/sonar-db-dao/src/schema/schema-sq.ddl b/server/sonar-db-dao/src/schema/schema-sq.ddl index 78f221a55e4..d2d54a7a8f7 100644 --- a/server/sonar-db-dao/src/schema/schema-sq.ddl +++ b/server/sonar-db-dao/src/schema/schema-sq.ddl @@ -126,6 +126,7 @@ CREATE TABLE "ARCHITECTURE_GRAPHS"( "GRAPH_DATA" CHARACTER LARGE OBJECT NOT NULL ); ALTER TABLE "ARCHITECTURE_GRAPHS" ADD CONSTRAINT "PK_ARCHITECTURE_GRAPHS" PRIMARY KEY("UUID"); +CREATE UNIQUE NULLS NOT DISTINCT INDEX "UQ_IDX_AG_BRANCH_TYPE_SOURCE" ON "ARCHITECTURE_GRAPHS"("BRANCH_UUID" NULLS FIRST, "TYPE" NULLS FIRST, "SOURCE" NULLS FIRST); CREATE TABLE "AUDITS"( "UUID" CHARACTER VARYING(40) NOT NULL, diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v108/MigratePortfoliosLiveMeasuresToMeasuresIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v108/MigratePortfoliosLiveMeasuresToMeasuresIT.java index 79dd5abe808..56acc2f70f9 100644 --- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v108/MigratePortfoliosLiveMeasuresToMeasuresIT.java +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v108/MigratePortfoliosLiveMeasuresToMeasuresIT.java @@ -282,7 +282,7 @@ class MigratePortfoliosLiveMeasuresToMeasuresIT { "component_uuid", componentUuid, "branch_uuid", branch, "json_value", "{\"any\":\"thing\"}", - "json_value_hash", "1234", + "json_value_hash", 1234, "created_at", 12, "updated_at", 12); } diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddProductionScopeToScaDependenciesTableIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/AddProductionScopeToScaDependenciesTableIT.java index b8ec749e66b..8ed2cb41454 100644 --- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/AddProductionScopeToScaDependenciesTableIT.java +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/AddProductionScopeToScaDependenciesTableIT.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.SQLException; import org.junit.jupiter.api.Test; diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnArchitectureGraphsIT.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnArchitectureGraphsIT.java new file mode 100644 index 00000000000..cc1d462e536 --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnArchitectureGraphsIT.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v202502; + +import java.sql.SQLException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.sonar.db.MigrationDbTester; +import org.sonar.server.platform.db.migration.step.DdlChange; + +import static org.sonar.db.MigrationDbTester.createForMigrationStep; +import static org.sonar.server.platform.db.migration.version.v202502.CreateUniqueIndexOnArchitectureGraphs.COLUMN_NAME_BRANCH_UUID; +import static org.sonar.server.platform.db.migration.version.v202502.CreateUniqueIndexOnArchitectureGraphs.COLUMN_NAME_SOURCE; +import static org.sonar.server.platform.db.migration.version.v202502.CreateUniqueIndexOnArchitectureGraphs.COLUMN_NAME_TYPE; +import static org.sonar.server.platform.db.migration.version.v202502.CreateUniqueIndexOnArchitectureGraphs.INDEX_NAME; +import static org.sonar.server.platform.db.migration.version.v202502.CreateUniqueIndexOnArchitectureGraphs.TABLE_NAME; + +class CreateIndexOnArchitectureGraphsIT { + + @RegisterExtension + public final MigrationDbTester db = createForMigrationStep(CreateUniqueIndexOnArchitectureGraphs.class); + private final DdlChange underTest = new CreateUniqueIndexOnArchitectureGraphs(db.database()); + + @Test + void execute_shouldCreateIndex() throws SQLException { + db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME); + underTest.execute(); + db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE); + } + + @Test + void execute_shouldBeReentrant() throws SQLException { + db.assertIndexDoesNotExist(TABLE_NAME, INDEX_NAME); + underTest.execute(); + underTest.execute(); + db.assertUniqueIndex(TABLE_NAME, INDEX_NAME, COLUMN_NAME_BRANCH_UUID, COLUMN_NAME_TYPE, COLUMN_NAME_SOURCE); + } +} diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaReleasesComponentUuidTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnScaReleasesComponentUuidTest.java index d7630686669..05857305b63 100644 --- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaReleasesComponentUuidTest.java +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnScaReleasesComponentUuidTest.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.SQLException; import org.junit.jupiter.api.Test; @@ -26,10 +26,10 @@ import org.sonar.db.MigrationDbTester; import org.sonar.server.platform.db.migration.step.DdlChange; import static org.sonar.db.MigrationDbTester.createForMigrationStep; -import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaReleasesComponentUuid.COLUMN_NAME_COMPONENT_UUID; -import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaReleasesComponentUuid.COLUMN_NAME_UUID; -import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaReleasesComponentUuid.INDEX_NAME; -import static org.sonar.server.platform.db.migration.version.v202503.CreateIndexOnScaReleasesComponentUuid.TABLE_NAME; +import static org.sonar.server.platform.db.migration.version.v202502.CreateIndexOnScaReleasesComponentUuid.COLUMN_NAME_COMPONENT_UUID; +import static org.sonar.server.platform.db.migration.version.v202502.CreateIndexOnScaReleasesComponentUuid.COLUMN_NAME_UUID; +import static org.sonar.server.platform.db.migration.version.v202502.CreateIndexOnScaReleasesComponentUuid.INDEX_NAME; +import static org.sonar.server.platform.db.migration.version.v202502.CreateIndexOnScaReleasesComponentUuid.TABLE_NAME; class CreateIndexOnScaReleasesComponentUuidTest { @RegisterExtension diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnScaReleasesComponentTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/DropIndexOnScaReleasesComponentTest.java index 97f8a4d557d..ec377d5fb96 100644 --- a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnScaReleasesComponentTest.java +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/version/v202502/DropIndexOnScaReleasesComponentTest.java @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.SQLException; import org.junit.jupiter.api.Test; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java index 5b5af55fd0b..f844369bf02 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java @@ -39,7 +39,6 @@ import org.sonar.server.platform.db.migration.version.v107.DbVersion107; import org.sonar.server.platform.db.migration.version.v108.DbVersion108; import org.sonar.server.platform.db.migration.version.v202501.DbVersion202501; import org.sonar.server.platform.db.migration.version.v202502.DbVersion202502; -import org.sonar.server.platform.db.migration.version.v202503.DbVersion202503; public class MigrationConfigurationModule extends Module { @Override @@ -59,7 +58,6 @@ public class MigrationConfigurationModule extends Module { DbVersion108.class, DbVersion202501.class, DbVersion202502.class, - DbVersion202503.class, // migration steps MigrationStepRegistryImpl.class, diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddDeclaredLicenseExpressionToScaReleasesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/AddDeclaredLicenseExpressionToScaReleasesTable.java index b8a0c26e52b..c44a3dc006c 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddDeclaredLicenseExpressionToScaReleasesTable.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/AddDeclaredLicenseExpressionToScaReleasesTable.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.SQLException; import org.sonar.db.Database; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddProductionScopeToScaDependenciesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/AddProductionScopeToScaDependenciesTable.java index 4764e60d61b..bf2aab43019 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/AddProductionScopeToScaDependenciesTable.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/AddProductionScopeToScaDependenciesTable.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.SQLException; import org.sonar.db.Database; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaReleasesComponentUuid.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnScaReleasesComponentUuid.java index 85dc6395842..848d5925a9e 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/CreateIndexOnScaReleasesComponentUuid.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateIndexOnScaReleasesComponentUuid.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import java.sql.Connection; import java.sql.SQLException; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java new file mode 100644 index 00000000000..802396d18b1 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/CreateUniqueIndexOnArchitectureGraphs.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.version.v202502; + +import java.sql.Connection; +import java.sql.SQLException; +import org.sonar.db.Database; +import org.sonar.db.DatabaseUtils; +import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder; +import org.sonar.server.platform.db.migration.step.DdlChange; + +public class CreateUniqueIndexOnArchitectureGraphs extends DdlChange { + + static final String TABLE_NAME = "architecture_graphs"; + static final String INDEX_NAME = "uq_idx_ag_branch_type_source"; + static final String COLUMN_NAME_BRANCH_UUID = "branch_uuid"; + static final String COLUMN_NAME_TYPE = "type"; + static final String COLUMN_NAME_SOURCE = "source"; + + public CreateUniqueIndexOnArchitectureGraphs(Database db) { + super(db); + } + + @Override + public void execute(Context context) throws SQLException { + try (Connection connection = getDatabase().getDataSource().getConnection()) { + createIndex(context, connection); + } + } + + private void createIndex(Context context, Connection connection) { + if (!DatabaseUtils.indexExistsIgnoreCase(TABLE_NAME, INDEX_NAME, connection)) { + context.execute(new CreateIndexBuilder(getDialect()) + .setTable(TABLE_NAME) + .setName(INDEX_NAME) + .setUnique(true) + .addColumn(COLUMN_NAME_BRANCH_UUID, false) + .addColumn(COLUMN_NAME_TYPE, false) + .addColumn(COLUMN_NAME_SOURCE, false) + .build()); + } + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DbVersion202502.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DbVersion202502.java index 0db5c95b6f3..8bbbe17ee01 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DbVersion202502.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DbVersion202502.java @@ -54,6 +54,10 @@ public class DbVersion202502 implements DbVersion { .add(2025_02_015, "Add new_in_pull_request column to SCA dependencies", AddNewInPullRequestToScaDependenciesTable.class) .add(2025_02_016, "Insert default AI Codefix provider key and modelKey properties", InsertDefaultAiSuggestionProviderKeyAndModelKeyProperties.class) .add(2025_02_017, "Add table 'architecture_graphs'", CreateArchitectureGraphsTable.class) - ; + .add(2025_02_018, "Drop 'sca_releases_comp_uuid' index", DropIndexOnScaReleasesComponent.class) + .add(2025_02_019, "Create 'sca_releases_comp_uuid_uuid' index", CreateIndexOnScaReleasesComponentUuid.class) + .add(2025_02_020, "Add 'sca_dependencies.production_scope' column", AddProductionScopeToScaDependenciesTable.class) + .add(2025_02_021, "Add declared_license_expression to SCA releases", AddDeclaredLicenseExpressionToScaReleasesTable.class) + .add(2025_02_022, "Create 'uq_idx_ag_branch_type_source' for architecture graphs", CreateUniqueIndexOnArchitectureGraphs.class); } } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnScaReleasesComponent.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DropIndexOnScaReleasesComponent.java index 8001e821463..5cec4743767 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DropIndexOnScaReleasesComponent.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202502/DropIndexOnScaReleasesComponent.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import org.sonar.db.Database; import org.sonar.server.platform.db.migration.step.DropIndexChange; diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java deleted file mode 100644 index c015f4a94ea..00000000000 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.platform.db.migration.version.v202503; - -import org.sonar.server.platform.db.migration.step.MigrationStepRegistry; -import org.sonar.server.platform.db.migration.version.DbVersion; - -// ignoring bad number formatting, as it's indented that we align the migration numbers to SQ versions -@SuppressWarnings("java:S3937") -public class DbVersion202503 implements DbVersion { - - /** - * We use the start of the 10.X cycle as an opportunity to align migration numbers with the SQ version number. - * Please follow this pattern: - * 2025_03_000 - * 2025_03_001 - * 2025_03_002 - */ - @Override - public void addSteps(MigrationStepRegistry registry) { - registry - .add(2025_03_000, "Drop 'sca_releases_comp_uuid' index", DropIndexOnScaReleasesComponent.class) - .add(2025_03_001, "Create 'sca_releases_comp_uuid_uuid' index", CreateIndexOnScaReleasesComponentUuid.class) - .add(2025_03_002, "Add 'sca_dependencies.production_scope' column", AddProductionScopeToScaDependenciesTable.class) - .add(2025_03_003, "Add declared_license_expression to SCA releases", AddDeclaredLicenseExpressionToScaReleasesTable.class); - } -} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/package-info.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/package-info.java deleted file mode 100644 index cc36bc37f03..00000000000 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v202503/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -@ParametersAreNonnullByDefault -package org.sonar.server.platform.db.migration.version.v202503; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202503/AddDeclaredLicenseExpressionToScaReleasesTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202502/AddDeclaredLicenseExpressionToScaReleasesTableTest.java index f2ff0411c79..182fcf41ecb 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202503/AddDeclaredLicenseExpressionToScaReleasesTableTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202502/AddDeclaredLicenseExpressionToScaReleasesTableTest.java @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -package org.sonar.server.platform.db.migration.version.v202503; +package org.sonar.server.platform.db.migration.version.v202502; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503Test.java deleted file mode 100644 index e1480dd9844..00000000000 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v202503/DbVersion202503Test.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.platform.db.migration.version.v202503; - -import org.junit.jupiter.api.Test; - -import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationNotEmpty; -import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber; - -class DbVersion202503Test { - - private final DbVersion202503 underTest = new DbVersion202503(); - - @Test - void migrationNumber_starts_at_2025_03_000() { - verifyMinimumMigrationNumber(underTest, 2025_03_000); - } - - @Test - void verify_migration_is_not_empty() { - verifyMigrationNotEmpty(underTest); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/setting/ThreadLocalSettings.java b/server/sonar-server-common/src/main/java/org/sonar/server/setting/ThreadLocalSettings.java index 92ffa498023..1786a62e90f 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/setting/ThreadLocalSettings.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/setting/ThreadLocalSettings.java @@ -20,13 +20,13 @@ package org.sonar.server.setting; import com.google.common.annotations.VisibleForTesting; +import jakarta.inject.Inject; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Properties; -import jakarta.inject.Inject; import org.apache.ibatis.exceptions.PersistenceException; import org.sonar.api.CoreProperties; import org.sonar.api.ce.ComputeEngineSide; diff --git a/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/Dimension.java b/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/Dimension.java index f3479858bff..4b136272f6c 100644 --- a/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/Dimension.java +++ b/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/Dimension.java @@ -31,7 +31,8 @@ public enum Dimension { USER("user"), PROJECT("project"), LANGUAGE("language"), - ANALYSIS("analysis"); + ANALYSIS("analysis"), + FIX_SUGGESTION("fixsuggestion"); private final String value; diff --git a/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/schema/FixSuggestionMetric.java b/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/schema/FixSuggestionMetric.java new file mode 100644 index 00000000000..e7b079a0b6d --- /dev/null +++ b/server/sonar-telemetry-core/src/main/java/org/sonar/telemetry/core/schema/FixSuggestionMetric.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.telemetry.core.schema; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.sonar.telemetry.core.TelemetryDataType; + +import static org.sonar.telemetry.core.Granularity.ADHOC; + +public class FixSuggestionMetric extends InstallationMetric { + + @JsonProperty("fix_suggestion_uuid") + private String fixSuggestionUuid; + + @JsonProperty("project_uuid") + private String projectUuid; + + public FixSuggestionMetric(String key, Object value, TelemetryDataType type, String projectUuid, String fixSuggestionUuid) { + super(key, value, type, ADHOC); + this.projectUuid = projectUuid; + this.fixSuggestionUuid = fixSuggestionUuid; + } + + public String getProjectUuid() { + return projectUuid; + } + + public void setProjectUuid(String projectUuid) { + this.projectUuid = projectUuid; + } + + public String getFixSuggestionUuid() { + return fixSuggestionUuid; + } + + public void setFixSuggestionUuid(String fixSuggestionUuid) { + this.fixSuggestionUuid = fixSuggestionUuid; + } +} diff --git a/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/DimensionTest.java b/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/DimensionTest.java index f80c2a971bb..69d77f3716f 100644 --- a/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/DimensionTest.java +++ b/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/DimensionTest.java @@ -39,11 +39,13 @@ class DimensionTest { assertEquals(Dimension.USER, Dimension.fromValue("user")); assertEquals(Dimension.PROJECT, Dimension.fromValue("project")); assertEquals(Dimension.LANGUAGE, Dimension.fromValue("language")); + assertEquals(Dimension.FIX_SUGGESTION, Dimension.fromValue("fixsuggestion")); assertEquals(Dimension.INSTALLATION, Dimension.fromValue("INSTALLATION")); assertEquals(Dimension.USER, Dimension.fromValue("USER")); assertEquals(Dimension.PROJECT, Dimension.fromValue("PROJECT")); assertEquals(Dimension.LANGUAGE, Dimension.fromValue("LANGUAGE")); + assertEquals(Dimension.FIX_SUGGESTION, Dimension.fromValue("FIXSUGGESTION")); } @Test diff --git a/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/schema/FixSuggestionMetricTest.java b/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/schema/FixSuggestionMetricTest.java new file mode 100644 index 00000000000..4169ca95089 --- /dev/null +++ b/server/sonar-telemetry-core/src/test/java/org/sonar/telemetry/core/schema/FixSuggestionMetricTest.java @@ -0,0 +1,51 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.telemetry.core.schema; + +import org.junit.jupiter.api.Test; +import org.sonar.telemetry.core.Granularity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.telemetry.core.TelemetryDataType.STRING; + +class FixSuggestionMetricTest { + + @Test + void getters() { + FixSuggestionMetric metric = new FixSuggestionMetric("ai_codefix.suggestion_rule_key", "rule:key", STRING, "projectUuid", "fixSuggestionUuid"); + + assertThat(metric.getKey()).isEqualTo("ai_codefix.suggestion_rule_key"); + assertThat(metric.getValue()).isEqualTo("rule:key"); + assertThat(metric.getProjectUuid()).isEqualTo("projectUuid"); + assertThat(metric.getGranularity()).isEqualTo(Granularity.ADHOC); + assertThat(metric.getType()).isEqualTo(STRING); + assertThat(metric.getFixSuggestionUuid()).isEqualTo("fixSuggestionUuid"); + } + + @Test + void setters() { + FixSuggestionMetric metric = new FixSuggestionMetric("ai_codefix.suggestion_rule_key", "rule:key", STRING, "projectUuid", "fixSuggestionUuid"); + metric.setProjectUuid("newProjectUuid"); + metric.setFixSuggestionUuid("newFixSuggestionUuid"); + + assertThat(metric.getProjectUuid()).isEqualTo("newProjectUuid"); + assertThat(metric.getFixSuggestionUuid()).isEqualTo("newFixSuggestionUuid"); + } +} diff --git a/server/sonar-webserver-api/src/it/java/org/sonar/server/plugins/DetectPluginChangeIT.java b/server/sonar-webserver-api/src/it/java/org/sonar/server/plugins/DetectPluginChangeIT.java index 174537211d5..51745d15781 100644 --- a/server/sonar-webserver-api/src/it/java/org/sonar/server/plugins/DetectPluginChangeIT.java +++ b/server/sonar-webserver-api/src/it/java/org/sonar/server/plugins/DetectPluginChangeIT.java @@ -19,6 +19,8 @@ */ package org.sonar.server.plugins; +import java.util.List; +import java.util.Map; import org.junit.Rule; import org.junit.Test; import org.sonar.api.Plugin; @@ -96,6 +98,35 @@ public class DetectPluginChangeIT { } @Test + public void detect_changes_when_forced_refresh() { + addPluginToDb("plugin1", "hash1", PluginDto.Type.BUNDLED); + addPluginToFs("plugin1", "hash1", PluginType.BUNDLED); + addPluginToDb("plugin2", "hash2", PluginDto.Type.EXTERNAL); + addPluginToFs("plugin2", "hash2", PluginType.EXTERNAL); + + dbTester.executeDdl("insert into internal_properties (kee, is_empty, text_value, created_at) values ('plugin.refresh.forced', false, 'true', 12345)"); + + detectPluginChange.start(); + assertThat(detectPluginChange.anyPluginChanged()).isTrue(); + + // Ensure the force refresh flag has been deleted + assertThat(getInternalProperty("plugin.refresh.forced")).isEmpty(); + } + + @Test + public void detect_changes_when_internal_propertiy_has_false_value_should_not_refresh() { + addPluginToDb("plugin1", "hash1", PluginDto.Type.BUNDLED); + addPluginToFs("plugin1", "hash1", PluginType.BUNDLED); + addPluginToDb("plugin2", "hash2", PluginDto.Type.EXTERNAL); + addPluginToFs("plugin2", "hash2", PluginType.EXTERNAL); + + dbTester.executeDdl("insert into internal_properties (kee, is_empty, text_value, created_at) values ('plugin.refresh.forced', false, 'false', 12345)"); + + detectPluginChange.start(); + assertThat(detectPluginChange.anyPluginChanged()).isFalse(); + } + + @Test public void fail_if_start_twice() { detectPluginChange.start(); assertThrows(IllegalStateException.class, detectPluginChange::start); @@ -123,4 +154,7 @@ public class DetectPluginChangeIT { pluginRepository.addPlugin(serverPlugin); } + private List<Map<String, Object>> getInternalProperty(String key) { + return dbTester.select("select * from internal_properties where kee='" + key + "'"); + } } diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/DetectPluginChange.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/DetectPluginChange.java index 40f8f8036d7..af1972430f9 100644 --- a/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/DetectPluginChange.java +++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/plugins/DetectPluginChange.java @@ -34,6 +34,7 @@ import org.sonar.db.plugin.PluginDto; import static java.util.function.Function.identity; public class DetectPluginChange implements Startable { + public static final String FORCE_PLUGIN_RELOAD_PROPERTY = "plugin.refresh.forced"; private static final Logger LOG = Loggers.get(DetectPluginChange.class); private final ServerPluginRepository serverPluginRepository; @@ -49,7 +50,7 @@ public class DetectPluginChange implements Startable { public void start() { Preconditions.checkState(changesDetected == null, "Can only call #start() once"); Profiler profiler = Profiler.create(LOG).startInfo("Detect plugin changes"); - changesDetected = anyChange(); + changesDetected = isForcedReload() || anyChange(); if (changesDetected) { LOG.debug("Plugin changes detected"); } else { @@ -65,6 +66,17 @@ public class DetectPluginChange implements Startable { return changesDetected; } + private boolean isForcedReload() { + try (DbSession dbSession = dbClient.openSession(false)) { + boolean forceRefresh = Boolean.parseBoolean(dbClient.internalPropertiesDao().selectByKey(dbSession, FORCE_PLUGIN_RELOAD_PROPERTY).orElse("false")); + if (forceRefresh) { + dbClient.internalPropertiesDao().delete(dbSession, FORCE_PLUGIN_RELOAD_PROPERTY); + dbSession.commit(); + } + return forceRefresh; + } + } + private boolean anyChange() { try (DbSession dbSession = dbClient.openSession(false)) { Map<String, PluginDto> dbPluginsByKey = dbClient.pluginDao().selectAll(dbSession).stream() diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/PropertiesDBCleaner.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/PropertiesDBCleaner.java new file mode 100644 index 00000000000..d4f87c46ce4 --- /dev/null +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/startup/PropertiesDBCleaner.java @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.sonar.api.SonarEdition; +import org.sonar.api.SonarRuntime; +import org.sonar.api.Startable; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; + +import static java.util.Arrays.asList; + +public class PropertiesDBCleaner implements Startable { + private static final Logger LOG = LoggerFactory.getLogger(PropertiesDBCleaner.class); + private final SonarRuntime runtime; + private final DbClient dbClient; + + public PropertiesDBCleaner(DbClient dbClient, SonarRuntime runtime) { + this.dbClient = dbClient; + this.runtime = runtime; + } + + @Override + public void start() { + LOG.info("Clean up properties from db"); + deleteMisraPropertyIfRequired(); + } + + private void deleteMisraPropertyIfRequired() { + String misraProperty = "sonar.earlyAccess.misra.enabled"; + SonarEdition edition = runtime.getEdition(); + try (DbSession dbSession = dbClient.openSession(false)) { + if (asList(SonarEdition.COMMUNITY, SonarEdition.DEVELOPER).contains(edition)) { + dbClient.propertiesDao().deleteGlobalProperty(misraProperty, dbSession); + dbSession.commit(); + } + } + } + + @Override + public void stop() { + // Nothing to do + } +} diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/PropertiesDBCleanerTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/PropertiesDBCleanerTest.java new file mode 100644 index 00000000000..15c9955f853 --- /dev/null +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/startup/PropertiesDBCleanerTest.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.startup; + +import java.util.Objects; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.sonar.api.SonarEdition; +import org.sonar.api.SonarRuntime; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.property.PropertyDto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +class PropertiesDBCleanerTest { + @RegisterExtension + public DbTester db = DbTester.create(); + private final DbClient dbClient = db.getDbClient(); + private final DbSession dbSession = db.getSession(); + private final SonarRuntime sonarRuntime = mock(SonarRuntime.class); + private static final String MISRA_SETTING = "sonar.earlyAccess.misra.enabled"; + + @ParameterizedTest + @ValueSource(strings = { "COMMUNITY", "DEVELOPER" }) + void should_clean_up_misra_prop_when_dev_or_community_edition(String edition) { + when(sonarRuntime.getEdition()).thenReturn(SonarEdition.valueOf(edition)); + + dbClient + .propertiesDao() + .saveProperty(dbSession, new PropertyDto() + .setKey(MISRA_SETTING) + .setValue("true"), null, null, null, null); + dbSession.commit(); + + new PropertiesDBCleaner(dbClient, sonarRuntime).start(); + assertThat(dbClient.propertiesDao().selectGlobalProperty(MISRA_SETTING)).isNull(); + } + + @ParameterizedTest + @ValueSource(strings = { "ENTERPRISE", "DATACENTER" }) + void should_not_clean_up_misra_prop_when_enterprise_or_above(String edition) { + when(sonarRuntime.getEdition()).thenReturn(SonarEdition.valueOf(edition)); + + PropertyDto prop = new PropertyDto() + .setKey(MISRA_SETTING) + .setValue("true"); + dbClient + .propertiesDao() + .saveProperty(dbSession, prop, null, null, null, null); + dbSession.commit(); + + new PropertiesDBCleaner(dbClient, sonarRuntime).start(); + assertThat(Objects.requireNonNull(dbClient.propertiesDao().selectGlobalProperty(MISRA_SETTING)).getValue()).isEqualTo(prop.getValue()); + } +} diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java index e9162a0c1f3..f8c11a99d6e 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/WebApiEndpoints.java @@ -21,7 +21,7 @@ package org.sonar.server.v2; public class WebApiEndpoints { public static final String JSON_MERGE_PATCH_CONTENT_TYPE = "application/merge-patch+json"; - public static final String INTERNAL = "internal"; + public static final String INTERNAL = "x-sonar-internal"; public static final String SYSTEM_DOMAIN = "/system"; public static final String LIVENESS_ENDPOINT = SYSTEM_DOMAIN + "/liveness"; diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentActionIT.java index 95a2e80093a..0c1e7a36271 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/measure/ws/ComponentActionIT.java @@ -33,6 +33,7 @@ import org.sonar.db.component.ProjectData; import org.sonar.db.component.SnapshotDto; import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; +import org.sonar.db.permission.GlobalPermission; import org.sonar.server.component.TestComponentFinder; import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; @@ -50,6 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES; import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.api.web.UserRole.SCAN; import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.BranchDto.DEFAULT_MAIN_BRANCH_NAME; import static org.sonar.db.component.BranchType.PULL_REQUEST; @@ -107,6 +109,32 @@ public class ComponentActionIT { } @Test + public void user_with_project_scan_permission_is_allowed_to_get_project_measures() { + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + userSession.addProjectPermission(SCAN, projectData.getProjectDto()) + .registerBranches(projectData.getMainBranchDto()); + MetricDto metric = db.measures().insertMetric(m -> m.setValueType("INT")); + + ComponentWsResponse response = newRequest(mainBranch.getKey(), metric.getKey()); + + assertThat(response.getMetrics().getMetricsCount()).isOne(); + } + + @Test + public void user_with_global_scan_permission_is_allowed_to_get_project_status() { + ProjectData projectData = db.components().insertPrivateProject(); + ComponentDto mainBranch = projectData.getMainBranchComponent(); + userSession.addPermission(GlobalPermission.SCAN); + + MetricDto metric = db.measures().insertMetric(m -> m.setValueType("INT")); + + ComponentWsResponse response = newRequest(mainBranch.getKey(), metric.getKey()); + + assertThat(response.getMetrics().getMetricsCount()).isOne(); + } + + @Test public void without_additional_fields() { ProjectData projectData = db.components().insertPrivateProject(); ComponentDto mainBranch = projectData.getMainBranchComponent(); diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/QProfileExportersIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/QProfileExportersIT.java deleted file mode 100644 index 9e06d87c0d6..00000000000 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/QProfileExportersIT.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.qualityprofile; - -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Collection; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.sonar.api.impl.utils.AlwaysIncreasingSystem2; -import org.sonar.api.profiles.ProfileExporter; -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.rules.Rule; -import org.sonar.api.rules.RuleFinder; -import org.sonar.api.rules.RulePriority; -import org.sonar.api.utils.System2; -import org.sonar.api.utils.ValidationMessages; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.db.rule.RuleDto; -import org.sonar.server.exceptions.BadRequestException; -import org.sonar.server.exceptions.NotFoundException; -import org.sonar.server.rule.DefaultRuleFinder; -import org.sonar.server.rule.RuleDescriptionFormatter; -import org.sonar.server.tester.UserSessionRule; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.io.IOUtils.toInputStream; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.sonar.db.qualityprofile.QualityProfileTesting.newQualityProfileDto; - -public class QProfileExportersIT { - - @org.junit.Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); - - private final System2 system2 = new AlwaysIncreasingSystem2(); - - @org.junit.Rule - public DbTester db = DbTester.create(system2); - - private final RuleFinder ruleFinder = new DefaultRuleFinder(db.getDbClient(), mock(RuleDescriptionFormatter.class)); - private final ProfileExporter[] exporters = new ProfileExporter[] { - new StandardExporter(), new XooExporter()}; - private final ProfileImporter[] importers = new ProfileImporter[] { - new XooProfileImporter(), new XooProfileImporterWithMessages(), new XooProfileImporterWithError()}; - private RuleDto rule; - private final QProfileRules qProfileRules = mock(QProfileRules.class); - private final QProfileExporters underTest = new QProfileExporters(db.getDbClient(), ruleFinder, qProfileRules, exporters, importers); - - @Before - public void setUp() { - rule = db.rules().insert(r -> r.setLanguage("xoo").setRepositoryKey("SonarXoo").setRuleKey("R1")); - } - - @Test - public void exportersForLanguage() { - assertThat(underTest.exportersForLanguage("xoo")).hasSize(2); - assertThat(underTest.exportersForLanguage("java")).hasSize(1); - assertThat(underTest.exportersForLanguage("java").get(0)).isInstanceOf(StandardExporter.class); - } - - @Test - public void mimeType() { - assertThat(underTest.mimeType("xootool")).isEqualTo("plain/custom"); - - // default mime type - assertThat(underTest.mimeType("standard")).isEqualTo("text/plain"); - } - - @Test - public void import_xml() { - QProfileDto profile = createProfile(); - - underTest.importXml(profile, "XooProfileImporter", toInputStream("<xml/>", UTF_8), db.getSession()); - - ArgumentCaptor<QProfileDto> profileCapture = ArgumentCaptor.forClass(QProfileDto.class); - Class<Collection<RuleActivation>> collectionClass = (Class<Collection<RuleActivation>>) (Class) Collection.class; - ArgumentCaptor<Collection<RuleActivation>> activationCapture = ArgumentCaptor.forClass(collectionClass); - verify(qProfileRules).activateAndCommit(any(DbSession.class), profileCapture.capture(), activationCapture.capture()); - - assertThat(profileCapture.getValue().getKee()).isEqualTo(profile.getKee()); - Collection<RuleActivation> activations = activationCapture.getValue(); - assertThat(activations).hasSize(1); - RuleActivation activation = activations.iterator().next(); - assertThat(activation.getRuleUuid()).isEqualTo(rule.getUuid()); - assertThat(activation.getSeverity()).isEqualTo("CRITICAL"); - } - - @Test - public void import_xml_return_messages() { - QProfileDto profile = createProfile(); - - QProfileResult result = underTest.importXml(profile, "XooProfileImporterWithMessages", toInputStream("<xml/>", UTF_8), db.getSession()); - - assertThat(result.infos()).containsOnly("an info"); - assertThat(result.warnings()).containsOnly("a warning"); - } - - @Test - public void fail_to_import_xml_when_error_in_importer() { - QProfileDto qProfileDto = newQualityProfileDto(); - InputStream inputStream = toInputStream("<xml/>", UTF_8); - DbSession dbSession = db.getSession(); - assertThatThrownBy(() -> underTest.importXml( - qProfileDto, "XooProfileImporterWithError", inputStream, dbSession)) - .isInstanceOf(BadRequestException.class) - .hasMessage("error!"); - } - - @Test - public void fail_to_import_xml_on_unknown_importer() { - QProfileDto qProfileDto = newQualityProfileDto(); - InputStream inputStream = toInputStream("<xml/>", UTF_8); - DbSession dbSession = db.getSession(); - assertThatThrownBy(() -> underTest.importXml(qProfileDto, "Unknown", inputStream, dbSession)) - .isInstanceOf(BadRequestException.class) - .hasMessage("No such importer : Unknown"); - } - - @Test - public void export_empty_profile() { - QProfileDto profile = createProfile(); - - StringWriter writer = new StringWriter(); - underTest.export(db.getSession(), profile, "standard", writer); - assertThat(writer).hasToString("standard -> " + profile.getName() + " -> 0"); - - writer = new StringWriter(); - underTest.export(db.getSession(), profile, "xootool", writer); - assertThat(writer).hasToString("xoo -> " + profile.getName() + " -> 0"); - } - - @Test - public void export_profile() { - QProfileDto profile = createProfile(); - db.qualityProfiles().activateRule(profile, rule); - - StringWriter writer = new StringWriter(); - underTest.export(db.getSession(), profile, "standard", writer); - assertThat(writer).hasToString("standard -> " + profile.getName() + " -> 1"); - - writer = new StringWriter(); - underTest.export(db.getSession(), profile, "xootool", writer); - assertThat(writer).hasToString("xoo -> " + profile.getName() + " -> 1"); - } - - @Test - public void export_throws_NotFoundException_if_exporter_does_not_exist() { - QProfileDto profile = createProfile(); - - assertThatThrownBy(() -> { - underTest.export(db.getSession(), profile, "does_not_exist", new StringWriter()); - }) - .isInstanceOf(NotFoundException.class) - .hasMessage("Unknown quality profile exporter: does_not_exist"); - } - - private QProfileDto createProfile() { - return db.qualityProfiles().insert(p -> p.setLanguage(rule.getLanguage())); - } - - public static class XooExporter extends ProfileExporter { - public XooExporter() { - super("xootool", "Xoo Tool"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[] {"xoo"}; - } - - @Override - public String getMimeType() { - return "plain/custom"; - } - - @Override - public void exportProfile(RulesProfile profile, Writer writer) { - try { - writer.write("xoo -> " + profile.getName() + " -> " + profile.getActiveRules().size()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - } - - public static class StandardExporter extends ProfileExporter { - public StandardExporter() { - super("standard", "Standard"); - } - - @Override - public void exportProfile(RulesProfile profile, Writer writer) { - try { - writer.write("standard -> " + profile.getName() + " -> " + profile.getActiveRules().size()); - } catch (IOException e) { - throw new IllegalStateException(e); - } - } - } - - public class XooProfileImporter extends ProfileImporter { - public XooProfileImporter() { - super("XooProfileImporter", "Xoo Profile Importer"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[] {"xoo"}; - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - RulesProfile rulesProfile = RulesProfile.create(); - rulesProfile.activateRule(Rule.create(rule.getRepositoryKey(), rule.getRuleKey()), RulePriority.CRITICAL); - return rulesProfile; - } - } - - public static class XooProfileImporterWithMessages extends ProfileImporter { - public XooProfileImporterWithMessages() { - super("XooProfileImporterWithMessages", "Xoo Profile Importer With Message"); - } - - @Override - public String[] getSupportedLanguages() { - return new String[] {}; - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - messages.addWarningText("a warning"); - messages.addInfoText("an info"); - return RulesProfile.create(); - } - } - - public static class XooProfileImporterWithError extends ProfileImporter { - public XooProfileImporterWithError() { - super("XooProfileImporterWithError", "Xoo Profile Importer With Error"); - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - messages.addErrorText("error!"); - return RulesProfile.create(); - } - } - -} diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/CreateActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/CreateActionIT.java index 395b4147c18..af594642a2e 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/CreateActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/CreateActionIT.java @@ -19,43 +19,20 @@ */ package org.sonar.server.qualityprofile.ws; -import com.google.common.collect.ImmutableMap; -import java.io.Reader; -import java.util.Collections; -import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.sonar.api.config.Configuration; -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.rules.RulePriority; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.System2; -import org.sonar.api.utils.ValidationMessages; -import org.sonar.api.utils.Version; -import org.sonar.core.platform.SonarQubeVersion; import org.sonar.core.util.UuidFactoryFast; -import org.sonar.core.util.UuidFactoryImpl; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.db.rule.RuleDto; -import org.sonar.db.rule.RuleTesting; import org.sonar.server.es.EsTester; -import org.sonar.server.exceptions.BadRequestException; import org.sonar.server.exceptions.ForbiddenException; -import org.sonar.server.pushapi.qualityprofile.QualityProfileChangeEventService; -import org.sonar.server.qualityprofile.QProfileExporters; import org.sonar.server.qualityprofile.QProfileFactoryImpl; -import org.sonar.server.qualityprofile.QProfileRules; -import org.sonar.server.qualityprofile.QProfileRulesImpl; -import org.sonar.server.qualityprofile.builtin.RuleActivator; import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; -import org.sonar.server.rule.index.RuleIndex; -import org.sonar.server.rule.index.RuleIndexer; -import org.sonar.server.rule.index.RuleQuery; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; @@ -67,7 +44,6 @@ import org.sonarqube.ws.Qualityprofiles.CreateWsResponse.QualityProfile; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES; import static org.sonar.db.permission.GlobalPermission.SCAN; import static org.sonar.server.language.LanguageTesting.newLanguages; @@ -75,9 +51,6 @@ import static org.sonar.server.language.LanguageTesting.newLanguages; class CreateActionIT { private static final String XOO_LANGUAGE = "xoo"; - private static final RuleDto RULE = RuleTesting.newXooX1() - .setSeverity("MINOR") - .setLanguage(XOO_LANGUAGE); @RegisterExtension private final DbTester db = DbTester.create(); @@ -86,21 +59,12 @@ class CreateActionIT { @RegisterExtension private final UserSessionRule userSession = UserSessionRule.standalone(); - private final Configuration config = mock(Configuration.class); private final DbClient dbClient = db.getDbClient(); private final DbSession dbSession = db.getSession(); - private final RuleIndex ruleIndex = new RuleIndex(es.client(), System2.INSTANCE, config); - private final RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbClient); private final ActiveRuleIndexer activeRuleIndexer = new ActiveRuleIndexer(dbClient, es.client()); - private final ProfileImporter[] profileImporters = createImporters(); - private final QualityProfileChangeEventService qualityProfileChangeEventService = mock(QualityProfileChangeEventService.class); - private final SonarQubeVersion sonarQubeVersion = new SonarQubeVersion(Version.create(10, 3)); - private final RuleActivator ruleActivator = new RuleActivator(System2.INSTANCE, dbClient, UuidFactoryImpl.INSTANCE, null, userSession, mock(Configuration.class), sonarQubeVersion); - private final QProfileRules qProfileRules = new QProfileRulesImpl(dbClient, ruleActivator, ruleIndex, activeRuleIndexer, qualityProfileChangeEventService); - private final QProfileExporters qProfileExporters = new QProfileExporters(dbClient, null, qProfileRules, profileImporters); private final CreateAction underTest = new CreateAction(dbClient, new QProfileFactoryImpl(dbClient, UuidFactoryFast.getInstance(), System2.INSTANCE, activeRuleIndexer), - qProfileExporters, newLanguages(XOO_LANGUAGE), userSession, activeRuleIndexer, profileImporters); + newLanguages(XOO_LANGUAGE), userSession); private WsActionTester ws = new WsActionTester(underTest); @@ -111,7 +75,7 @@ class CreateActionIT { assertThat(definition.responseExampleAsString()).isNotEmpty(); assertThat(definition.isPost()).isTrue(); assertThat(definition.params()).extracting(Param::key) - .containsExactlyInAnyOrder("language", "name", "backup_with_messages", "backup_with_errors", "backup_xoo_lint"); + .containsExactlyInAnyOrder("language", "name"); } @Test @@ -136,30 +100,6 @@ class CreateActionIT { } @Test - void create_profile_from_backup_xml() { - logInAsQProfileAdministrator(); - insertRule(RULE); - - executeRequest("New Profile", XOO_LANGUAGE, ImmutableMap.of("xoo_lint", "<xml/>")); - - QProfileDto dto = dbClient.qualityProfileDao().selectByNameAndLanguage(dbSession, "New Profile", XOO_LANGUAGE); - assertThat(dto.getKee()).isNotNull(); - assertThat(dbClient.activeRuleDao().selectByProfileUuid(dbSession, dto.getKee())).hasSize(1); - assertThat(ruleIndex.searchAll(new RuleQuery().setQProfile(dto).setActivation(true))).toIterable().hasSize(1); - } - - @Test - void create_profile_with_messages() { - logInAsQProfileAdministrator(); - - CreateWsResponse response = executeRequest("Profile with messages", XOO_LANGUAGE, ImmutableMap.of("with_messages", "<xml/>")); - - QualityProfile profile = response.getProfile(); - assertThat(profile.getInfos().getInfosList()).containsOnly("an info"); - assertThat(profile.getWarnings().getWarningsList()).containsOnly("a warning"); - } - - @Test void fail_if_unsufficient_privileges() { userSession .logIn() @@ -175,16 +115,6 @@ class CreateActionIT { } @Test - void fail_if_import_generate_error() { - logInAsQProfileAdministrator(); - - assertThatThrownBy(() -> { - executeRequest("Profile with errors", XOO_LANGUAGE, ImmutableMap.of("with_errors", "<xml/>")); - }) - .isInstanceOf(BadRequestException.class); - } - - @Test void test_json() { logInAsQProfileAdministrator(); @@ -199,23 +129,10 @@ class CreateActionIT { assertThat(response.getMediaType()).isEqualTo(MediaTypes.JSON); } - private void insertRule(RuleDto ruleDto) { - dbClient.ruleDao().insert(dbSession, ruleDto); - dbSession.commit(); - ruleIndexer.commitAndIndex(dbSession, ruleDto.getUuid()); - } - private CreateWsResponse executeRequest(String name, String language) { - return executeRequest(name, language, Collections.emptyMap()); - } - - private CreateWsResponse executeRequest(String name, String language, Map<String, String> xmls) { TestRequest request = ws.newRequest() .setParam("name", name) .setParam("language", language); - for (Map.Entry<String, String> entry : xmls.entrySet()) { - request.setParam("backup_" + entry.getKey(), entry.getValue()); - } return executeRequest(request); } @@ -223,55 +140,6 @@ class CreateActionIT { return request.executeProtobuf(CreateWsResponse.class); } - private ProfileImporter[] createImporters() { - class DefaultProfileImporter extends ProfileImporter { - private DefaultProfileImporter() { - super("xoo_lint", "Xoo Lint"); - setSupportedLanguages(XOO_LANGUAGE); - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - RulesProfile rulesProfile = RulesProfile.create(); - rulesProfile.activateRule(org.sonar.api.rules.Rule.create(RULE.getRepositoryKey(), RULE.getRuleKey()), RulePriority.BLOCKER); - return rulesProfile; - } - } - - class ProfileImporterGeneratingMessages extends ProfileImporter { - private ProfileImporterGeneratingMessages() { - super("with_messages", "With messages"); - setSupportedLanguages(XOO_LANGUAGE); - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - RulesProfile rulesProfile = RulesProfile.create(); - messages.addWarningText("a warning"); - messages.addInfoText("an info"); - return rulesProfile; - } - } - - class ProfileImporterGeneratingErrors extends ProfileImporter { - private ProfileImporterGeneratingErrors() { - super("with_errors", "With errors"); - setSupportedLanguages(XOO_LANGUAGE); - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - RulesProfile rulesProfile = RulesProfile.create(); - messages.addErrorText("error!"); - return rulesProfile; - } - } - - return new ProfileImporter[] { - new DefaultProfileImporter(), new ProfileImporterGeneratingMessages(), new ProfileImporterGeneratingErrors() - }; - } - private void logInAsQProfileAdministrator() { userSession .logIn() diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ExportActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ExportActionIT.java index 8b33646673d..c22c3ddd306 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ExportActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/qualityprofile/ws/ExportActionIT.java @@ -23,11 +23,8 @@ import java.io.IOException; import java.io.Reader; import java.io.Writer; import javax.annotation.Nullable; -import org.apache.commons.lang3.StringUtils; import org.junit.Rule; import org.junit.Test; -import org.sonar.api.profiles.ProfileExporter; -import org.sonar.api.profiles.RulesProfile; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.db.DbClient; @@ -37,7 +34,6 @@ import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.language.LanguageTesting; import org.sonar.server.qualityprofile.QProfileBackuper; -import org.sonar.server.qualityprofile.QProfileExporters; import org.sonar.server.qualityprofile.QProfileRestoreSummary; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; @@ -60,39 +56,10 @@ public class ExportActionIT { private final QProfileBackuper backuper = new TestBackuper(); @Test - public void export_profile() { + public void return_backup() { QProfileDto profile = createProfile(false); - WsActionTester tester = newWsActionTester(newExporter("polop"), newExporter("palap")); - String result = tester.newRequest() - .setParam("language", profile.getLanguage()) - .setParam("qualityProfile", profile.getName()) - .setParam("exporterKey", "polop").execute() - .getInput(); - - assertThat(result).isEqualTo("Profile " + profile.getLanguage() + "/" + profile.getName() + " exported by polop"); - } - - @Test - public void export_default_profile() { - QProfileDto nonDefaultProfile = createProfile(false); - QProfileDto defaultProfile = createProfile(true); - - WsActionTester tester = newWsActionTester(newExporter("polop"), newExporter("palap")); - String result = tester.newRequest() - .setParam("language", XOO_LANGUAGE) - .setParam("exporterKey", "polop") - .execute() - .getInput(); - - assertThat(result).isEqualTo("Profile " + defaultProfile.getLanguage() + "/" + defaultProfile.getName() + " exported by polop"); - } - - @Test - public void return_backup_when_exporter_is_not_specified() { - QProfileDto profile = createProfile(false); - - String result = newWsActionTester(newExporter("polop")).newRequest() + String result = newWsActionTester().newRequest() .setParam("language", profile.getLanguage()) .setParam("qualityProfile", profile.getName()) .execute() @@ -112,20 +79,7 @@ public class ExportActionIT { } @Test - public void throw_IAE_if_export_with_specified_key_does_not_exist() { - QProfileDto profile = createProfile(true); - - assertThatThrownBy(() -> { - newWsActionTester(newExporter("polop"), newExporter("palap")).newRequest() - .setParam("language", XOO_LANGUAGE) - .setParam("exporterKey", "unknown").execute(); - }) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Value of parameter 'exporterKey' (unknown) must be one of: [polop, palap]"); - } - - @Test - public void definition_without_exporters() { + public void definition() { WebService.Action definition = newWsActionTester().getDef(); assertThat(definition.isPost()).isFalse(); @@ -139,18 +93,6 @@ public class ExportActionIT { assertThat(language.deprecatedSince()).isNullOrEmpty(); } - @Test - public void definition_with_exporters() { - WebService.Action definition = newWsActionTester(newExporter("polop"), newExporter("palap")).getDef(); - - assertThat(definition.isPost()).isFalse(); - assertThat(definition.isInternal()).isFalse(); - assertThat(definition.params()).extracting("key").containsExactlyInAnyOrder("language", "qualityProfile", "exporterKey"); - WebService.Param exportersParam = definition.param("exporterKey"); - assertThat(exportersParam.possibleValues()).containsOnly("polop", "palap"); - assertThat(exportersParam.isInternal()).isFalse(); - } - private QProfileDto createProfile(boolean isDefault) { QProfileDto profile = db.qualityProfiles().insert(p -> p.setLanguage(XOO_LANGUAGE)); if (isDefault) { @@ -159,27 +101,8 @@ public class ExportActionIT { return profile; } - private WsActionTester newWsActionTester(ProfileExporter... profileExporters) { - QProfileExporters exporters = new QProfileExporters(dbClient, null, null, profileExporters, null); - return new WsActionTester(new ExportAction(dbClient, backuper, exporters, LanguageTesting.newLanguages(XOO_LANGUAGE, JAVA_LANGUAGE))); - } - - private static ProfileExporter newExporter(String key) { - return new ProfileExporter(key, StringUtils.capitalize(key)) { - @Override - public String getMimeType() { - return "text/plain+" + key; - } - - @Override - public void exportProfile(RulesProfile profile, Writer writer) { - try { - writer.write(format("Profile %s/%s exported by %s", profile.getLanguage(), profile.getName(), key)); - } catch (IOException ioe) { - throw new RuntimeException(ioe); - } - } - }; + private WsActionTester newWsActionTester() { + return new WsActionTester(new ExportAction(dbClient, backuper, LanguageTesting.newLanguages(XOO_LANGUAGE, JAVA_LANGUAGE))); } private static class TestBackuper implements QProfileBackuper { diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/CurrentActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/CurrentActionIT.java index b2a716df087..f343d99f2b2 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/CurrentActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/CurrentActionIT.java @@ -27,18 +27,18 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Suite; -import org.sonar.db.component.ComponentQualifiers; -import org.sonar.server.component.ComponentType; -import org.sonar.server.component.ComponentTypeTree; -import org.sonar.server.component.ComponentTypes; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.core.platform.PlatformEditionProvider; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentQualifiers; import org.sonar.db.property.PropertyDto; import org.sonar.db.user.UserDto; import org.sonar.server.common.avatar.AvatarResolverImpl; +import org.sonar.server.component.ComponentType; +import org.sonar.server.component.ComponentTypeTree; +import org.sonar.server.component.ComponentTypes; import org.sonar.server.permission.PermissionService; import org.sonar.server.permission.PermissionServiceImpl; import org.sonar.server.tester.UserSessionRule; @@ -55,7 +55,6 @@ import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFIL import static org.sonar.db.permission.GlobalPermission.PROVISION_PROJECTS; import static org.sonar.db.permission.GlobalPermission.SCAN; import static org.sonar.db.user.GroupTesting.newGroupDto; -import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS; import static org.sonar.test.JsonAssert.assertJson; @RunWith(Suite.class) @@ -262,7 +261,7 @@ public class CurrentActionIT { @Parameterized.Parameters public static Collection<String> parameterCombination() { - return AVAILABLE_NOTICE_KEYS; + return DismissNoticeAction.DismissNotices.getAvailableKeys(); } private final String notice; diff --git a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java index a3c6748a49a..4d0c6acf8b5 100644 --- a/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java +++ b/server/sonar-webserver-webapi/src/it/java/org/sonar/server/user/ws/DismissNoticeActionIT.java @@ -20,12 +20,12 @@ package org.sonar.server.user.ws; import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Optional; -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import org.sonar.api.utils.System2; import org.sonar.db.DbTester; import org.sonar.db.property.PropertyDto; @@ -37,20 +37,18 @@ import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS; -@RunWith(DataProviderRunner.class) -public class DismissNoticeActionIT { +class DismissNoticeActionIT { - @Rule - public DbTester db = DbTester.create(System2.INSTANCE); - @Rule - public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @RegisterExtension + private DbTester db = DbTester.create(System2.INSTANCE); + @RegisterExtension + private UserSessionRule userSessionRule = UserSessionRule.standalone(); private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient())); @Test - public void authentication_is_required() { + void authentication_is_required() { TestRequest testRequest = tester.newRequest() .setParam("notice", "anyValue"); @@ -60,7 +58,7 @@ public class DismissNoticeActionIT { } @Test - public void notice_parameter_is_mandatory() { + void notice_parameter_is_mandatory() { userSessionRule.logIn(); TestRequest testRequest = tester.newRequest(); @@ -70,20 +68,19 @@ public class DismissNoticeActionIT { } @Test - public void notice_not_supported() { + void notice_not_supported() { userSessionRule.logIn(); TestRequest testRequest = tester.newRequest() .setParam("notice", "not_supported_value"); assertThatThrownBy(testRequest::execute) .isInstanceOf(IllegalArgumentException.class) - .hasMessage( - "Value of parameter 'notice' (not_supported_value) must be one of: [educationPrinciples, sonarlintAd, issueCleanCodeGuide, qualityGateCaYCConditionsSimplification, " + - "overviewZeroNewIssuesSimplification, issueNewIssueStatusAndTransitionGuide, onboardingDismissCaycBranchSummaryGuide, showNewModesTour, showNewModesBanner]"); + .hasMessageStartingWith( + "Value of parameter 'notice' (not_supported_value) must be one of: ["); } @Test - public void notice_already_exist_dont_fail() { + void notice_already_exist_dont_fail() { userSessionRule.logIn(); PropertyDto property = new PropertyDto().setKey("user.dismissedNotices.educationPrinciples").setUserUuid(userSessionRule.getUuid()); db.properties().insertProperties(userSessionRule.getLogin(), null, null, null, property); @@ -97,9 +94,9 @@ public class DismissNoticeActionIT { assertThat(db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.educationPrinciples")).isPresent(); } - @Test - @UseDataProvider("noticeKeys") - public void dismiss_notice(String noticeKey) { + @ParameterizedTest + @MethodSource("noticeKeys") + void dismiss_notice(String noticeKey) { userSessionRule.logIn(); TestResponse testResponse = tester.newRequest() @@ -113,9 +110,7 @@ public class DismissNoticeActionIT { } @DataProvider - public static Object[][] noticeKeys() { - return AVAILABLE_NOTICE_KEYS.stream() - .map(noticeKey -> new Object[] {noticeKey}) - .toArray(Object[][]::new); + static Set<String> noticeKeys() { + return DismissNoticeAction.DismissNotices.getAvailableKeys(); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentAction.java index 0f52c689e69..f57d384d416 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentAction.java @@ -42,6 +42,7 @@ import org.sonar.db.component.SnapshotDto; import org.sonar.db.measure.MeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.metric.MetricDtoFunctions; +import org.sonar.db.permission.GlobalPermission; import org.sonar.server.component.ComponentFinder; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.user.UserSession; @@ -66,6 +67,7 @@ import static org.sonar.server.measure.ws.ComponentResponseCommon.addMetricToRes import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createAdditionalFieldsParameter; import static org.sonar.server.measure.ws.MeasuresWsParametersBuilder.createMetricKeysParameter; import static org.sonar.server.measure.ws.SnapshotDtoToWsPeriod.snapshotToWsPeriods; +import static org.sonar.server.user.AbstractUserSession.insufficientPrivilegesException; import static org.sonar.server.ws.KeyExamples.KEY_BRANCH_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PROJECT_EXAMPLE_001; import static org.sonar.server.ws.KeyExamples.KEY_PULL_REQUEST_EXAMPLE_001; @@ -88,10 +90,15 @@ public class ComponentAction implements MeasuresWsAction { public void define(WebService.NewController context) { WebService.NewAction action = context.createAction(ACTION_COMPONENT) .setDescription("Return component with specified measures.<br>" + - "Requires the following permission: 'Browse' on the project of specified component.") + "Requires one of the following permissions:" + + "<ul>" + + "<li>'Browse' on the project of the specified component</li>" + + "<li>'Execute Analysis' on the project of the specified component</li>" + + "</ul>") .setResponseExample(getClass().getResource("component-example.json")) .setSince("5.4") .setChangelog( + new Change("2025.2", "The 'Execute Analysis' permission also allows to access the endpoint"), new Change("10.8", format("The following metrics are not deprecated anymore: %s", MeasuresWsModule.getUndeprecatedMetricsinSonarQube108())), new Change("10.8", String.format("Added new accepted values for the 'metricKeys' param: %s", @@ -282,7 +289,11 @@ public class ComponentAction implements MeasuresWsAction { } private void checkPermissions(ComponentDto baseComponent) { - userSession.checkComponentPermission(UserRole.USER, baseComponent); + if (!userSession.hasComponentPermission(UserRole.USER, baseComponent) && + !userSession.hasComponentPermission(UserRole.SCAN, baseComponent) && + !userSession.hasPermission(GlobalPermission.SCAN)) { + throw insufficientPrivilegesException(); + } } private static class ComponentRequest { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileExporters.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileExporters.java deleted file mode 100644 index a6f2fc4bcc0..00000000000 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileExporters.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.qualityprofile; - -import com.google.common.collect.FluentIterable; -import com.google.common.collect.ListMultimap; -import com.google.common.collect.Lists; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; -import javax.annotation.CheckForNull; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.sonar.api.profiles.ProfileExporter; -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; -import org.sonar.api.rule.RuleKey; -import org.sonar.api.rules.ActiveRule; -import org.sonar.api.rules.ActiveRuleParam; -import org.sonar.api.rules.Rule; -import org.sonar.api.rules.RuleFinder; -import org.sonar.api.rules.RulePriority; -import org.sonar.api.server.ServerSide; -import org.sonar.api.utils.ValidationMessages; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.qualityprofile.ActiveRuleDto; -import org.sonar.db.qualityprofile.ActiveRuleParamDto; -import org.sonar.db.qualityprofile.OrgActiveRuleDto; -import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.db.rule.RuleDto; -import org.sonar.server.exceptions.BadRequestException; -import org.sonar.server.exceptions.NotFoundException; -import org.springframework.beans.factory.annotation.Autowired; - -import static org.sonar.server.exceptions.BadRequestException.checkRequest; - -@ServerSide -public class QProfileExporters { - - private final DbClient dbClient; - private final RuleFinder ruleFinder; - private final QProfileRules qProfileRules; - private final ProfileExporter[] exporters; - private final ProfileImporter[] importers; - - @Autowired(required = false) - public QProfileExporters(DbClient dbClient, RuleFinder ruleFinder, QProfileRules qProfileRules, ProfileExporter[] exporters, ProfileImporter[] importers) { - this.dbClient = dbClient; - this.ruleFinder = ruleFinder; - this.qProfileRules = qProfileRules; - this.exporters = exporters; - this.importers = importers; - } - - /** - * Used by the ioc container if no {@link ProfileImporter} is found - */ - @Autowired(required = false) - public QProfileExporters(DbClient dbClient, RuleFinder ruleFinder, QProfileRules qProfileRules, ProfileExporter[] exporters) { - this(dbClient, ruleFinder, qProfileRules, exporters, new ProfileImporter[0]); - } - - /** - * Used by the ioc container if no {@link ProfileExporter} is found - */ - @Autowired(required = false) - public QProfileExporters(DbClient dbClient, RuleFinder ruleFinder, QProfileRules qProfileRules, ProfileImporter[] importers) { - this(dbClient, ruleFinder, qProfileRules, new ProfileExporter[0], importers); - } - - /** - * Used by the ioc container if no {@link ProfileImporter} nor {@link ProfileExporter} is found - */ - @Autowired(required = false) - public QProfileExporters(DbClient dbClient, RuleFinder ruleFinder, QProfileRules qProfileRules) { - this(dbClient, ruleFinder, qProfileRules, new ProfileExporter[0], new ProfileImporter[0]); - } - - public List<ProfileExporter> exportersForLanguage(String language) { - List<ProfileExporter> result = new ArrayList<>(); - for (ProfileExporter exporter : exporters) { - if (exporter.getSupportedLanguages() == null || exporter.getSupportedLanguages().length == 0 || ArrayUtils.contains(exporter.getSupportedLanguages(), language)) { - result.add(exporter); - } - } - return result; - } - - public String mimeType(String exporterKey) { - ProfileExporter exporter = findExporter(exporterKey); - return exporter.getMimeType(); - } - - public void export(DbSession dbSession, QProfileDto profile, String exporterKey, Writer writer) { - ProfileExporter exporter = findExporter(exporterKey); - exporter.exportProfile(wrap(dbSession, profile), writer); - } - - private RulesProfile wrap(DbSession dbSession, QProfileDto profile) { - RulesProfile target = new RulesProfile(profile.getName(), profile.getLanguage()); - List<OrgActiveRuleDto> activeRuleDtos = dbClient.activeRuleDao().selectByProfile(dbSession, profile); - List<ActiveRuleParamDto> activeRuleParamDtos = dbClient.activeRuleDao().selectParamsByActiveRuleUuids(dbSession, Lists.transform(activeRuleDtos, ActiveRuleDto::getUuid)); - ListMultimap<String, ActiveRuleParamDto> activeRuleParamsByActiveRuleUuid = FluentIterable.from(activeRuleParamDtos).index(ActiveRuleParamDto::getActiveRuleUuid); - - for (ActiveRuleDto activeRule : activeRuleDtos) { - // TODO all rules should be loaded by using one query with all active rule keys as parameter - Rule rule = ruleFinder.findByKey(activeRule.getRuleKey()); - org.sonar.api.rules.ActiveRule wrappedActiveRule = target.activateRule(rule, RulePriority.valueOf(activeRule.getSeverityString())); - List<ActiveRuleParamDto> paramDtos = activeRuleParamsByActiveRuleUuid.get(activeRule.getUuid()); - for (ActiveRuleParamDto activeRuleParamDto : paramDtos) { - wrappedActiveRule.setParameter(activeRuleParamDto.getKey(), activeRuleParamDto.getValue()); - } - } - return target; - } - - private ProfileExporter findExporter(String exporterKey) { - for (ProfileExporter e : exporters) { - if (exporterKey.equals(e.getKey())) { - return e; - } - } - throw new NotFoundException("Unknown quality profile exporter: " + exporterKey); - } - - public QProfileResult importXml(QProfileDto profile, String importerKey, InputStream xml, DbSession dbSession) { - return importXml(profile, importerKey, new InputStreamReader(xml, StandardCharsets.UTF_8), dbSession); - } - - private QProfileResult importXml(QProfileDto profile, String importerKey, Reader xml, DbSession dbSession) { - QProfileResult result = new QProfileResult(); - ValidationMessages messages = ValidationMessages.create(); - ProfileImporter importer = getProfileImporter(importerKey); - RulesProfile definition = importer.importProfile(xml, messages); - List<ActiveRuleChange> changes = importProfile(profile, definition, dbSession); - result.addChanges(changes); - processValidationMessages(messages, result); - return result; - } - - private List<ActiveRuleChange> importProfile(QProfileDto profile, RulesProfile definition, DbSession dbSession) { - Map<RuleKey, RuleDto> rulesByRuleKey = dbClient.ruleDao().selectAll(dbSession) - .stream() - .collect(Collectors.toMap(RuleDto::getKey, Function.identity())); - List<ActiveRule> activeRules = definition.getActiveRules(); - List<RuleActivation> activations = activeRules.stream() - .map(activeRule -> toRuleActivation(activeRule, rulesByRuleKey)) - .filter(Objects::nonNull) - .toList(); - return qProfileRules.activateAndCommit(dbSession, profile, activations); - } - - private ProfileImporter getProfileImporter(String importerKey) { - for (ProfileImporter importer : importers) { - if (StringUtils.equals(importerKey, importer.getKey())) { - return importer; - } - } - throw BadRequestException.create("No such importer : " + importerKey); - } - - private static void processValidationMessages(ValidationMessages messages, QProfileResult result) { - checkRequest(messages.getErrors().isEmpty(), messages.getErrors()); - result.addWarnings(messages.getWarnings()); - result.addInfos(messages.getInfos()); - } - - @CheckForNull - private static RuleActivation toRuleActivation(ActiveRule activeRule, Map<RuleKey, RuleDto> rulesByRuleKey) { - RuleKey ruleKey = activeRule.getRule().ruleKey(); - RuleDto ruleDto = rulesByRuleKey.get(ruleKey); - if (ruleDto == null) { - return null; - } - String severity = activeRule.getSeverity().name(); - Map<String, String> params = activeRule.getActiveRuleParams().stream() - .collect(Collectors.toMap(ActiveRuleParam::getKey, ActiveRuleParam::getValue)); - return RuleActivation.create(ruleDto.getUuid(), severity, params); - } - -} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileResult.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileResult.java deleted file mode 100644 index 2dadd2d8015..00000000000 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/QProfileResult.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2025 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.qualityprofile; - -import java.util.ArrayList; -import java.util.List; -import org.sonar.db.qualityprofile.QProfileDto; - -public class QProfileResult { - - private List<String> warnings; - private List<String> infos; - - private QProfileDto profile; - - private List<ActiveRuleChange> changes; - - public QProfileResult() { - warnings = new ArrayList<>(); - infos = new ArrayList<>(); - changes = new ArrayList<>(); - } - - public List<String> warnings() { - return warnings; - } - - public QProfileResult addWarnings(List<String> warnings) { - this.warnings.addAll(warnings); - return this; - } - - public List<String> infos() { - return infos; - } - - public QProfileResult addInfos(List<String> infos) { - this.infos.addAll(infos); - return this; - } - - public QProfileDto profile() { - return profile; - } - - public QProfileResult setProfile(QProfileDto profile) { - this.profile = profile; - return this; - } - - public List<ActiveRuleChange> getChanges() { - return changes; - } - - public QProfileResult addChanges(List<ActiveRuleChange> changes) { - this.changes.addAll(changes); - return this; - } - - public QProfileResult add(QProfileResult result) { - warnings.addAll(result.warnings()); - infos.addAll(result.infos()); - changes.addAll(result.getChanges()); - return this; - } - -} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java index 5a5b0d07f77..dd20b9c78c8 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/builtin/BuiltInQProfileRepositoryImpl.java @@ -32,7 +32,6 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; -import org.sonar.api.profiles.RulesProfile; import org.sonar.api.resources.Language; import org.sonar.api.resources.Languages; import org.sonar.api.rule.RuleKey; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/CreateAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/CreateAction.java index bd38a0f3d2e..2c391027a21 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/CreateAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/CreateAction.java @@ -19,13 +19,8 @@ */ package org.sonar.server.qualityprofile.ws; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; import javax.annotation.Nullable; -import org.sonar.api.profiles.ProfileImporter; import org.sonar.api.resources.Languages; -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; @@ -33,11 +28,8 @@ import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.qualityprofile.QProfileDto; -import org.sonar.server.qualityprofile.QProfileExporters; import org.sonar.server.qualityprofile.QProfileFactory; import org.sonar.server.qualityprofile.builtin.QProfileName; -import org.sonar.server.qualityprofile.QProfileResult; -import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; import org.sonar.server.user.UserSession; import org.sonarqube.ws.Qualityprofiles.CreateWsResponse; @@ -48,37 +40,22 @@ import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.ACTION_CREATE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_LANGUAGE; import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters.PARAM_NAME; -import org.springframework.beans.factory.annotation.Autowired; public class CreateAction implements QProfileWsAction { - private static final String PARAM_BACKUP_FORMAT = "backup_%s"; static final int NAME_MAXIMUM_LENGTH = 100; private final DbClient dbClient; private final QProfileFactory profileFactory; - private final QProfileExporters exporters; private final Languages languages; - private final ProfileImporter[] importers; private final UserSession userSession; - private final ActiveRuleIndexer activeRuleIndexer; - @Autowired(required = false) - public CreateAction(DbClient dbClient, QProfileFactory profileFactory, QProfileExporters exporters, Languages languages, - UserSession userSession, ActiveRuleIndexer activeRuleIndexer, ProfileImporter... importers) { + public CreateAction(DbClient dbClient, QProfileFactory profileFactory, Languages languages, + UserSession userSession) { this.dbClient = dbClient; this.profileFactory = profileFactory; - this.exporters = exporters; this.languages = languages; this.userSession = userSession; - this.activeRuleIndexer = activeRuleIndexer; - this.importers = importers; - } - - @Autowired(required = false) - public CreateAction(DbClient dbClient, QProfileFactory profileFactory, QProfileExporters exporters, Languages languages, - UserSession userSession, ActiveRuleIndexer activeRuleIndexer) { - this(dbClient, profileFactory, exporters, languages, userSession, activeRuleIndexer, new ProfileImporter[0]); } @Override @@ -90,7 +67,6 @@ public class CreateAction implements QProfileWsAction { .setResponseExample(getClass().getResource("create-example.json")) .setSince("5.2") .setHandler(this); - List<Change> changelog = new ArrayList<>(); create.createParam(PARAM_NAME) .setRequired(true) @@ -103,41 +79,22 @@ public class CreateAction implements QProfileWsAction { .setDescription("Quality profile language") .setExampleValue("js") .setPossibleValues(getOrderedLanguageKeys(languages)); - - for (ProfileImporter importer : importers) { - String backupParamName = getBackupParamName(importer.getKey()); - create.createParam(backupParamName) - .setDescription(String.format("A configuration file for %s.", importer.getName())) - .setDeprecatedSince("9.8"); - changelog.add(new Change("9.8", String.format("'%s' parameter is deprecated", backupParamName))); - } - - create.setChangelog(changelog.toArray(new Change[0])); } @Override public void handle(Request request, Response response) throws Exception { userSession.checkLoggedIn(); + userSession.checkPermission(ADMINISTER_QUALITY_PROFILES); try (DbSession dbSession = dbClient.openSession(false)) { - userSession.checkPermission(ADMINISTER_QUALITY_PROFILES); CreateRequest createRequest = toRequest(request); - writeProtobuf(doHandle(dbSession, createRequest, request), request, response); + writeProtobuf(doHandle(dbSession, createRequest), request, response); } } - private CreateWsResponse doHandle(DbSession dbSession, CreateRequest createRequest, Request request) { - QProfileResult result = new QProfileResult(); + private CreateWsResponse doHandle(DbSession dbSession, CreateRequest createRequest) { QProfileDto profile = profileFactory.checkAndCreateCustom(dbSession, QProfileName.createFor(createRequest.getLanguage(), createRequest.getName())); - result.setProfile(profile); - for (ProfileImporter importer : importers) { - String importerKey = importer.getKey(); - InputStream contentToImport = request.paramAsInputStream(getBackupParamName(importerKey)); - if (contentToImport != null) { - result.add(exporters.importXml(profile, importerKey, contentToImport, dbSession)); - } - } - activeRuleIndexer.commitAndIndex(dbSession, result.getChanges()); - return buildResponse(result); + dbSession.commit(); + return buildResponse(profile); } private static CreateRequest toRequest(Request request) { @@ -147,28 +104,18 @@ public class CreateAction implements QProfileWsAction { return builder.build(); } - private CreateWsResponse buildResponse(QProfileResult result) { - String language = result.profile().getLanguage(); + private CreateWsResponse buildResponse(QProfileDto profile) { + String language = profile.getLanguage(); CreateWsResponse.QualityProfile.Builder builder = CreateWsResponse.QualityProfile.newBuilder() - .setKey(result.profile().getKee()) - .setName(result.profile().getName()) + .setKey(profile.getKee()) + .setName(profile.getName()) .setLanguage(language) - .setLanguageName(languages.get(result.profile().getLanguage()).getName()) + .setLanguageName(languages.get(profile.getLanguage()).getName()) .setIsDefault(false) .setIsInherited(false); - if (!result.infos().isEmpty()) { - builder.getInfosBuilder().addAllInfos(result.infos()); - } - if (!result.warnings().isEmpty()) { - builder.getWarningsBuilder().addAllWarnings(result.warnings()); - } return CreateWsResponse.newBuilder().setProfile(builder.build()).build(); } - private static String getBackupParamName(String importerKey) { - return String.format(PARAM_BACKUP_FORMAT, importerKey); - } - private static class CreateRequest { private final String name; private final String language; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportAction.java index ce8e5c5bdc0..bd39c10a605 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportAction.java @@ -24,14 +24,10 @@ import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; -import java.util.Arrays; -import java.util.Collection; -import java.util.Set; -import java.util.stream.Collectors; import javax.annotation.Nullable; import org.apache.commons.io.IOUtils; -import org.sonar.api.profiles.ProfileExporter; import org.sonar.api.resources.Languages; +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.Response.Stream; @@ -42,7 +38,6 @@ import org.sonar.db.DbSession; import org.sonar.db.qualityprofile.QProfileDto; import org.sonar.server.language.LanguageParamUtils; import org.sonar.server.qualityprofile.QProfileBackuper; -import org.sonar.server.qualityprofile.QProfileExporters; import org.sonarqube.ws.MediaTypes; import static java.nio.charset.StandardCharsets.UTF_8; @@ -52,17 +47,13 @@ import static org.sonarqube.ws.client.qualityprofile.QualityProfileWsParameters. public class ExportAction implements QProfileWsAction { - private static final String PARAM_EXPORTER_KEY = "exporterKey"; - private final DbClient dbClient; private final QProfileBackuper backuper; - private final QProfileExporters exporters; private final Languages languages; - public ExportAction(DbClient dbClient, QProfileBackuper backuper, QProfileExporters exporters, Languages languages) { + public ExportAction(DbClient dbClient, QProfileBackuper backuper, Languages languages) { this.dbClient = dbClient; this.backuper = backuper; - this.exporters = exporters; this.languages = languages; } @@ -72,7 +63,10 @@ public class ExportAction implements QProfileWsAction { .setSince("5.2") .setDescription("Export a quality profile.") .setResponseExample(getClass().getResource("export-example.xml")) - .setHandler(this); + .setHandler(this) + .setDeprecatedSince("25.4") + .setChangelog( + new Change("25.4", "Deprecated. Use GET /api/qualityprofiles/backup instead")); action.createParam(PARAM_QUALITY_PROFILE) .setDescription("Quality profile name to export. If left empty, the default profile for the language is exported.") @@ -84,17 +78,6 @@ public class ExportAction implements QProfileWsAction { .setExampleValue(LanguageParamUtils.getExampleValue(languages)) .setPossibleValues(LanguageParamUtils.getOrderedLanguageKeys(languages)); - Set<String> exporterKeys = Arrays.stream(languages.all()) - .map(language -> exporters.exportersForLanguage(language.getKey())) - .flatMap(Collection::stream) - .map(ProfileExporter::getKey) - .collect(Collectors.toSet()); - if (!exporterKeys.isEmpty()) { - action.createParam(PARAM_EXPORTER_KEY) - .setDescription("Output format. If left empty, the same format as api/qualityprofiles/backup is used. " + - "Possible values are described by api/qualityprofiles/exporters.") - .setPossibleValues(exporterKeys); - } } @Override @@ -104,22 +87,16 @@ public class ExportAction implements QProfileWsAction { try (DbSession dbSession = dbClient.openSession(false)) { QProfileDto profile = loadProfile(dbSession, language, name); - String exporterKey = exporters.exportersForLanguage(profile.getLanguage()).isEmpty() ? null : request.param(PARAM_EXPORTER_KEY); - writeResponse(dbSession, profile, exporterKey, response); + writeResponse(dbSession, profile, response); } } - private void writeResponse(DbSession dbSession, QProfileDto profile, @Nullable String exporterKey, Response response) throws IOException { + private void writeResponse(DbSession dbSession, QProfileDto profile, Response response) throws IOException { Stream stream = response.stream(); ByteArrayOutputStream bufferStream = new ByteArrayOutputStream(); try (Writer writer = new OutputStreamWriter(bufferStream, UTF_8)) { - if (exporterKey == null) { - stream.setMediaType(MediaTypes.XML); - backuper.backup(dbSession, profile, writer); - } else { - stream.setMediaType(exporters.mimeType(exporterKey)); - exporters.export(dbSession, profile, exporterKey, writer); - } + stream.setMediaType(MediaTypes.XML); + backuper.backup(dbSession, profile, writer); } OutputStream output = response.stream().output(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportersAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportersAction.java index de3190bcf3f..57a318e4761 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportersAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ExportersAction.java @@ -19,53 +19,27 @@ */ package org.sonar.server.qualityprofile.ws; -import org.sonar.api.profiles.ProfileExporter; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService.NewController; import org.sonar.api.utils.text.JsonWriter; -import org.springframework.beans.factory.annotation.Autowired; public class ExportersAction implements QProfileWsAction { - private final ProfileExporter[] exporters; - - @Autowired(required = false) - public ExportersAction(ProfileExporter[] exporters) { - this.exporters = exporters; - } - - /** - * Used by the container if no {@link ProfileExporter} is found - */ - @Autowired(required = false) - public ExportersAction() { - this(new ProfileExporter[0]); - } - @Override public void define(NewController context) { context.createAction("exporters") - .setDescription("Lists available profile export formats.") + .setDescription("Deprecated. No more custom profile exporters.") .setHandler(this) .setResponseExample(getClass().getResource("exporters-example.json")) - .setSince("5.2"); + .setSince("5.2") + .setDeprecatedSince("25.4"); } @Override public void handle(Request request, Response response) throws Exception { try (JsonWriter json = response.newJsonWriter()) { json.beginObject().name("exporters").beginArray(); - for (ProfileExporter exporter : exporters) { - json.beginObject() - .prop("key", exporter.getKey()) - .prop("name", exporter.getName()); - json.name("languages").beginArray(); - for (String language : exporter.getSupportedLanguages()) { - json.value(language); - } - json.endArray().endObject(); - } json.endArray().endObject(); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ImportersAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ImportersAction.java index 078434db363..3d93cc675bb 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ImportersAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualityprofile/ws/ImportersAction.java @@ -19,50 +19,28 @@ */ package org.sonar.server.qualityprofile.ws; -import org.sonar.api.profiles.ProfileImporter; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.text.JsonWriter; -import org.springframework.beans.factory.annotation.Autowired; public class ImportersAction implements QProfileWsAction { - private final ProfileImporter[] importers; - - @Autowired(required = false) - public ImportersAction(ProfileImporter[] importers) { - this.importers = importers; - } - - @Autowired(required = false) - public ImportersAction() { - this(new ProfileImporter[0]); - } - @Override public void define(WebService.NewController controller) { controller.createAction("importers") + .setDescription("Deprecated. No more custom profile importers.") .setSince("5.2") .setDescription("List supported importers.") .setResponseExample(getClass().getResource("importers-example.json")) - .setHandler(this); + .setHandler(this) + .setDeprecatedSince("25.4"); } @Override public void handle(Request request, Response response) throws Exception { try (JsonWriter json = response.newJsonWriter()) { json.beginObject().name("importers").beginArray(); - for (ProfileImporter importer : importers) { - json.beginObject() - .prop("key", importer.getKey()) - .prop("name", importer.getName()) - .name("languages").beginArray(); - for (String languageKey : importer.getSupportedLanguages()) { - json.value(languageKey); - } - json.endArray().endObject(); - } json.endArray().endObject(); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CurrentAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CurrentAction.java index 36a5c376f48..aabb5ff63a4 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CurrentAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CurrentAction.java @@ -49,7 +49,6 @@ import static java.util.Optional.of; import static java.util.Optional.ofNullable; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.sonar.api.web.UserRole.USER; -import static org.sonar.server.user.ws.DismissNoticeAction.AVAILABLE_NOTICE_KEYS; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.Users.CurrentWsResponse.HomepageType.APPLICATION; import static org.sonarqube.ws.Users.CurrentWsResponse.HomepageType.PORTFOLIO; @@ -124,7 +123,8 @@ public class CurrentAction implements UsersWsAction { .setHomepage(buildHomepage(dbSession, user)) .setUsingSonarLintConnectedMode(user.getLastSonarlintConnectionDate() != null); - AVAILABLE_NOTICE_KEYS.forEach(key -> builder.putDismissedNotices(key, isNoticeDismissed(user, key))); + DismissNoticeAction.DismissNotices.getAvailableKeys() + .forEach(key -> builder.putDismissedNotices(key, isNoticeDismissed(user, key))); ofNullable(emptyToNull(user.getEmail())).ifPresent(builder::setEmail); ofNullable(emptyToNull(user.getEmail())).ifPresent(u -> builder.setAvatar(avatarResolver.create(user))); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java index 7a779e06308..407ffcf2f36 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java @@ -19,7 +19,9 @@ */ package org.sonar.server.user.ws; -import java.util.List; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -31,23 +33,52 @@ import org.sonar.db.property.PropertyQuery; import org.sonar.server.user.UserSession; import static com.google.common.base.Preconditions.checkState; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.EDUCATION_PRINCIPLES; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.ISSUE_CLEAN_CODE_GUIDE; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_BANNER; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_OPTIN_BANNER; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_DNA_TOUR; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_BANNER; +import static org.sonar.server.user.ws.DismissNoticeAction.DismissNotices.SHOW_NEW_MODES_TOUR; public class DismissNoticeAction implements UsersWsAction { - private static final String EDUCATION_PRINCIPLES = "educationPrinciples"; - private static final String SONARLINT_AD = "sonarlintAd"; - private static final String ISSUE_CLEAN_CODE_GUIDE = "issueCleanCodeGuide"; - private static final String QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION = "qualityGateCaYCConditionsSimplification"; - private static final String OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION = "overviewZeroNewIssuesSimplification"; - private static final String ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE = "issueNewIssueStatusAndTransitionGuide"; - private static final String ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE = "onboardingDismissCaycBranchSummaryGuide"; - private static final String SHOW_NEW_MODES_TOUR = "showNewModesTour"; - private static final String SHOW_NEW_MODES_BANNER = "showNewModesBanner"; - - protected static final List<String> AVAILABLE_NOTICE_KEYS = List.of(EDUCATION_PRINCIPLES, SONARLINT_AD, ISSUE_CLEAN_CODE_GUIDE, QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION, - OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION, ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE, ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE, SHOW_NEW_MODES_TOUR, SHOW_NEW_MODES_BANNER); + public enum DismissNotices { + EDUCATION_PRINCIPLES("educationPrinciples"), + SONARLINT_AD("sonarlintAd"), + ISSUE_CLEAN_CODE_GUIDE("issueCleanCodeGuide"), + QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION("qualityGateCaYCConditionsSimplification"), + OVERVIEW_ZERO_NEW_ISSUES_SIMPLIFICATION("overviewZeroNewIssuesSimplification"), + ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE("issueNewIssueStatusAndTransitionGuide"), + ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE("onboardingDismissCaycBranchSummaryGuide"), + SHOW_NEW_MODES_TOUR("showNewModesTour"), + SHOW_NEW_MODES_BANNER("showNewModesBanner"), + SHOW_DNA_OPTIN_BANNER("showDesignAndArchitectureOptInBanner"), + SHOW_DNA_BANNER("showDesignAndArchitectureBanner"), + SHOW_DNA_TOUR("showDesignAndArchitectureTour"), + ; + + private final String key; + + DismissNotices(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public static Set<String> getAvailableKeys() { + return Arrays.stream(values()) + .map(DismissNotices::getKey) + .collect(Collectors.toSet()); + } + } public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices."; - public static final String SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added."; + private static final String SUPPORT_FOR_NEW_NOTICE_MESSAGE = "Support for new notice '%s' was added."; private final UserSession userSession; private final DbClient dbClient; @@ -57,16 +88,29 @@ public class DismissNoticeAction implements UsersWsAction { this.dbClient = dbClient; } + private static String printNewNotice(DismissNotices notice) { + return SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(notice.getKey()); + } + + private static String printNewNotice(DismissNotices... notices) { + String noticesList = Arrays.stream(notices) + .map(DismissNotices::getKey) + .collect(Collectors.joining(", ")); + return SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(noticesList); + } + + @Override public void define(WebService.NewController context) { WebService.NewAction action = context.createAction("dismiss_notice") .setDescription("Dismiss a notice for the current user. Silently ignore if the notice is already dismissed.") - .setChangelog(new Change("10.8", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(SHOW_NEW_MODES_TOUR))) - .setChangelog(new Change("10.8", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(SHOW_NEW_MODES_BANNER))) - .setChangelog(new Change("10.6", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE))) - .setChangelog(new Change("10.4", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE))) - .setChangelog(new Change("10.3", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION))) - .setChangelog(new Change("10.2", SUPPORT_FOR_NEW_NOTICE_MESSAGE.formatted(ISSUE_CLEAN_CODE_GUIDE))) + .setChangelog(new Change("25.4", printNewNotice(SHOW_DNA_OPTIN_BANNER, SHOW_DNA_BANNER, SHOW_DNA_TOUR))) + .setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_TOUR))) + .setChangelog(new Change("10.8", printNewNotice(SHOW_NEW_MODES_BANNER))) + .setChangelog(new Change("10.6", printNewNotice(ONBOARDING_CAYC_BRANCH_SUMMARY_GUIDE))) + .setChangelog(new Change("10.4", printNewNotice(ISSUE_NEW_ISSUE_STATUS_AND_TRANSITION_GUIDE))) + .setChangelog(new Change("10.3", printNewNotice(QUALITY_GATE_CAYC_CONDITIONS_SIMPLIFICATION))) + .setChangelog(new Change("10.2", printNewNotice(ISSUE_CLEAN_CODE_GUIDE))) .setSince("9.6") .setInternal(true) .setHandler(this) @@ -75,7 +119,7 @@ public class DismissNoticeAction implements UsersWsAction { action.createParam("notice") .setDescription("notice key to dismiss") .setExampleValue(EDUCATION_PRINCIPLES) - .setPossibleValues(AVAILABLE_NOTICE_KEYS); + .setPossibleValues(DismissNotices.getAvailableKeys()); } @Override @@ -89,7 +133,7 @@ public class DismissNoticeAction implements UsersWsAction { dismissNotice(response, currentUserUuid, noticeKeyParam); } - public void dismissNotice(Response response, String currentUserUuid, String noticeKeyParam) { + private void dismissNotice(Response response, String currentUserUuid, String noticeKeyParam) { try (DbSession dbSession = dbClient.openSession(false)) { String paramKey = USER_DISMISS_CONSTANT + noticeKeyParam; PropertyQuery query = new PropertyQuery.Builder() diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/create-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/create-example.json index b154d6233ae..a1f54c5aa82 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/create-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/create-example.json @@ -6,11 +6,5 @@ "languageName" : "Java", "name" : "My New Profile", "key" : "AU-TpxcA-iU5OvuD2FL1" - }, - "warnings" : [ - "Unable to import unknown PMD rule 'rulesets/java/strings.xml'", - "Unable to import unknown PMD rule 'rulesets/java/basic.xml/UnnecessaryConversionTemporary'", - "Unable to import unknown PMD rule 'rulesets/java/basic.xml/EmptyCatchBlock'", - "Unable to import unknown PMD rule 'rulesets/java/braces.xml'" - ] + } } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/exporters-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/exporters-example.json index 642a2bde09d..2d1cd2df442 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/exporters-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/exporters-example.json @@ -1,33 +1,4 @@ { "exporters": [ - { - "key": "pmd", - "name": "PMD", - "languages": [ - "java" - ] - }, - { - "key": "checkstyle", - "name": "Checkstyle", - "languages": [ - "java" - ] - }, - { - "key": "js-lint", - "name": "JS Lint", - "languages": [ - "js" - ] - }, - { - "key": "android-lint", - "name": "Android Lint", - "languages": [ - "xml", - "java" - ] - } ] } diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/importers-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/importers-example.json index 8750609887a..a6d01b82776 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/importers-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualityprofile/ws/importers-example.json @@ -1,33 +1,4 @@ { "importers": [ - { - "key": "pmd", - "name": "PMD", - "languages": [ - "java" - ] - }, - { - "key": "checkstyle", - "name": "Checkstyle", - "languages": [ - "java" - ] - }, - { - "key": "js-lint", - "name": "JS Lint", - "languages": [ - "js" - ] - }, - { - "key": "android-lint", - "name": "Android Lint", - "languages": [ - "xml", - "java" - ] - } ] } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ExportersActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ExportersActionTest.java index 4ab5db1b9ba..d2298c7f0d5 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ExportersActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ExportersActionTest.java @@ -19,10 +19,7 @@ */ package org.sonar.server.qualityprofile.ws; -import java.io.Writer; import org.junit.Test; -import org.sonar.api.profiles.ProfileExporter; -import org.sonar.api.profiles.RulesProfile; import org.sonar.api.server.ws.WebService; import org.sonar.server.ws.WsActionTester; @@ -30,7 +27,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.sonar.test.JsonAssert.assertJson; public class ExportersActionTest { - private WsActionTester ws = new WsActionTester(new ExportersAction(createExporters())); + private final WsActionTester ws = new WsActionTester(new ExportersAction()); @Test public void importers_nominal() { @@ -48,24 +45,4 @@ public class ExportersActionTest { assertThat(exporters.responseExampleAsString()).isNotEmpty(); } - private ProfileExporter[] createExporters() { - class NoopImporter extends ProfileExporter { - private NoopImporter(String key, String name, String... languages) { - super(key, name); - setSupportedLanguages(languages); - } - - @Override - public void exportProfile(RulesProfile profile, Writer writer) { - // Nothing - } - - } - return new ProfileExporter[] { - new NoopImporter("pmd", "PMD", "java"), - new NoopImporter("checkstyle", "Checkstyle", "java"), - new NoopImporter("js-lint", "JS Lint", "js"), - new NoopImporter("android-lint", "Android Lint", "xml", "java") - }; - } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ImportersActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ImportersActionTest.java index 067a394af5a..63918caa13f 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ImportersActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualityprofile/ws/ImportersActionTest.java @@ -19,12 +19,8 @@ */ package org.sonar.server.qualityprofile.ws; -import java.io.Reader; import org.junit.Test; -import org.sonar.api.profiles.ProfileImporter; -import org.sonar.api.profiles.RulesProfile; import org.sonar.api.server.ws.WebService; -import org.sonar.api.utils.ValidationMessages; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; @@ -32,7 +28,7 @@ import static org.sonar.test.JsonAssert.assertJson; public class ImportersActionTest { - private WsActionTester ws = new WsActionTester(new ImportersAction(createImporters())); + private WsActionTester ws = new WsActionTester(new ImportersAction()); @Test public void empty_importers() { @@ -59,25 +55,4 @@ public class ImportersActionTest { assertThat(importers.responseExampleAsString()).isNotEmpty(); } - private ProfileImporter[] createImporters() { - class NoopImporter extends ProfileImporter { - private NoopImporter(String key, String name, String... languages) { - super(key, name); - setSupportedLanguages(languages); - } - - @Override - public RulesProfile importProfile(Reader reader, ValidationMessages messages) { - return RulesProfile.create(); - } - - } - - return new ProfileImporter[] { - new NoopImporter("pmd", "PMD", "java"), - new NoopImporter("checkstyle", "Checkstyle", "java"), - new NoopImporter("js-lint", "JS Lint", "js"), - new NoopImporter("android-lint", "Android Lint", "xml", "java") - }; - } } diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java index 6d29800e273..8d6253b5e58 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java @@ -31,6 +31,7 @@ import org.sonar.server.platform.serverid.ServerIdModule; import org.sonar.server.plugins.DetectPluginChange; import org.sonar.server.setting.DatabaseSettingLoader; import org.sonar.server.setting.DatabaseSettingsEnabler; +import org.sonar.server.startup.PropertiesDBCleaner; import static org.sonar.core.extension.CoreExtensionsInstaller.noAdditionalSideFilter; import static org.sonar.core.extension.PlatformLevelPredicates.hasPlatformLevel; @@ -49,6 +50,7 @@ public class PlatformLevel3 extends PlatformLevel { NoopDatabaseMigrationImpl.class, new ServerIdModule(), ServerImpl.class, + PropertiesDBCleaner.class, DatabaseSettingLoader.class, DatabaseSettingsEnabler.class, UriReader.class, diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java index a9885cbf8d6..cb5cb05bc90 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java @@ -248,7 +248,6 @@ import org.sonar.server.qualitygate.ws.QualityGateWsModule; import org.sonar.server.qualityprofile.QProfileBackuperImpl; import org.sonar.server.qualityprofile.QProfileComparison; import org.sonar.server.qualityprofile.QProfileCopier; -import org.sonar.server.qualityprofile.QProfileExporters; import org.sonar.server.qualityprofile.QProfileFactoryImpl; import org.sonar.server.qualityprofile.QProfileParser; import org.sonar.server.qualityprofile.QProfileResetImpl; @@ -372,7 +371,6 @@ public class PlatformLevel4 extends PlatformLevel { QProfileRulesImpl.class, RuleActivator.class, QualityProfileChangeEventServiceImpl.class, - QProfileExporters.class, QProfileFactoryImpl.class, QProfileCopier.class, QProfileBackuperImpl.class, diff --git a/settings.gradle b/settings.gradle index e8b44803d22..bcdc1176596 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,10 +17,6 @@ pluginManagement { } } } - plugins { - id 'com.bmuschko.docker-remote-api' version '9.4.0' - id 'org.ajoberstar.grgit' version '4.1.1' - } } rootProject.name = 'sonarqube' diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java index b4fd5512335..e84f2d3d5c8 100644 --- a/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java +++ b/sonar-plugin-api-impl/src/main/java/org/sonar/api/batch/sensor/internal/SensorContextTester.java @@ -19,8 +19,6 @@ */ package org.sonar.api.batch.sensor.internal; -import static java.util.Collections.unmodifiableMap; - import java.io.File; import java.io.InputStream; import java.io.Serializable; @@ -90,6 +88,8 @@ import org.sonar.api.scanner.fs.InputProject; import org.sonar.api.utils.System2; import org.sonar.api.utils.Version; +import static java.util.Collections.unmodifiableMap; + /** * Utility class to help testing {@link Sensor}. This is not an API and method signature may evolve. * <p> @@ -462,4 +462,5 @@ public class SensorContextTester implements SensorContext { public NewSignificantCode newSignificantCode() { return new DefaultSignificantCode(sensorStorage); } + } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java index 6f67c7afa98..9c41891dbab 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/bootstrap/SpringScannerContainer.java @@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory; import org.sonar.api.batch.fs.internal.FileMetadata; import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.batch.sensor.issue.internal.DefaultNoSonarFilter; -import org.sonar.api.config.PropertyDefinition; import org.sonar.api.scan.filesystem.PathResolver; import org.sonar.core.extension.CoreExtensionsInstaller; import org.sonar.core.metric.ScannerMetrics; @@ -154,25 +153,10 @@ public class SpringScannerContainer extends SpringComponentContainer { @Override protected void doBeforeStart() { - addSuffixesDeprecatedProperties(); addScannerExtensions(); addComponents(); } - private void addSuffixesDeprecatedProperties() { - add( - /* - * This is needed to support properly the deprecated sonar.rpg.suffixes property when the download optimization feature is enabled. - * The value of the property is needed at the preprocessing stage, but being defined by an optional analyzer means that at preprocessing - * it won't be properly available. This will be removed in SQ 11.0 together with the drop of the property from the rpg analyzer. - * See SONAR-21514 - */ - PropertyDefinition.builder("sonar.rpg.file.suffixes") - .deprecatedKey("sonar.rpg.suffixes") - .multiValues(true) - .build()); - } - private void addScannerExtensions() { getParentComponentByType(CoreExtensionsInstaller.class) .install(this, noExtensionFilter(), extension -> getScannerProjectExtensionsFilter().accept(extension)); diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java index 1e37a715066..df9bfd00b48 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/IssuePublisher.java @@ -24,6 +24,7 @@ import java.util.Collection; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; @@ -54,6 +55,7 @@ import org.sonar.scanner.report.ReportPublisher; @ThreadSafe public class IssuePublisher { + private static final Set<String> noSonarKeyContains = Set.of("nosonar", "S1291"); private final ActiveRules activeRules; private final IssueFilters filters; private final ReportPublisher reportPublisher; @@ -91,7 +93,7 @@ public class IssuePublisher { return inputComponent.isFile() && textRange != null && ((DefaultInputFile) inputComponent).hasNoSonarAt(textRange.start().line()) - && !StringUtils.containsIgnoreCase(issue.ruleKey().rule(), "nosonar"); + && noSonarKeyContains.stream().noneMatch(k -> StringUtils.containsIgnoreCase(issue.ruleKey().rule(), k)); } public void initAndAddExternalIssue(ExternalIssue issue) { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java index 7ee00d8e08f..8a8e90cce25 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/CliService.java @@ -21,12 +21,19 @@ package org.sonar.scanner.sca; import java.io.File; import java.io.IOException; +import java.io.StringWriter; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; @@ -36,6 +43,8 @@ import org.sonar.api.utils.System2; import org.sonar.core.util.ProcessWrapperFactory; import org.sonar.scanner.config.DefaultConfiguration; import org.sonar.scanner.repository.TelemetryCache; +import org.sonar.scanner.scm.ScmConfiguration; +import org.sonar.scm.git.JGitUtils; /** * The CliService class is meant to serve as the main entrypoint for any commands @@ -45,24 +54,26 @@ import org.sonar.scanner.repository.TelemetryCache; */ public class CliService { private static final Logger LOG = LoggerFactory.getLogger(CliService.class); + public static final String EXCLUDED_MANIFESTS_PROP_KEY = "sonar.sca.excludedManifests"; + private final ProcessWrapperFactory processWrapperFactory; private final TelemetryCache telemetryCache; private final System2 system2; private final Server server; + private final ScmConfiguration scmConfiguration; - public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server) { + public CliService(ProcessWrapperFactory processWrapperFactory, TelemetryCache telemetryCache, System2 system2, Server server, ScmConfiguration scmConfiguration) { this.processWrapperFactory = processWrapperFactory; this.telemetryCache = telemetryCache; this.system2 = system2; this.server = server; + this.scmConfiguration = scmConfiguration; } public File generateManifestsZip(DefaultInputModule module, File cliExecutable, DefaultConfiguration configuration) throws IOException, IllegalStateException { long startTime = system2.now(); boolean success = false; try { - var debugLevel = Level.DEBUG; - String zipName = "dependency-files.zip"; Path zipPath = module.getWorkDir().resolve(zipName); List<String> args = new ArrayList<>(); @@ -75,29 +86,32 @@ public class CliService { args.add("--directory"); args.add(module.getBaseDir().toString()); + String excludeFlag = getExcludeFlag(module, configuration); + if (excludeFlag != null) { + args.add("--exclude"); + args.add(excludeFlag); + } + boolean scaDebug = configuration.getBoolean("sonar.sca.debug").orElse(false); if (LOG.isDebugEnabled() || scaDebug) { LOG.info("Setting CLI to debug mode"); args.add("--debug"); - if (scaDebug) { - // output --debug logs from stderr to the info level logger - debugLevel = Level.INFO; - } } - LOG.atLevel(debugLevel).log("Calling ProcessBuilder with args: {}", args); - Map<String, String> envProperties = new HashMap<>(); // sending this will tell the CLI to skip checking for the latest available version on startup envProperties.put("TIDELIFT_SKIP_UPDATE_CHECK", "1"); envProperties.put("TIDELIFT_ALLOW_MANIFEST_FAILURES", "1"); envProperties.put("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE", "1"); envProperties.put("TIDELIFT_CLI_SQ_SERVER_VERSION", server.getVersion()); - envProperties.putAll(ScaProperties.buildFromScannerProperties(configuration)); + // EXCLUDED_MANIFESTS_PROP_KEY is a special case which we handle via --args, not environment variables + Set<String> ignoredProperties = Set.of(EXCLUDED_MANIFESTS_PROP_KEY); + envProperties.putAll(ScaProperties.buildFromScannerProperties(configuration, ignoredProperties)); - LOG.atLevel(debugLevel).log("Environment properties: {}", envProperties); + LOG.info("Running command: {}", args); + LOG.info("Environment properties: {}", envProperties); - Consumer<String> logConsumer = LOG.atLevel(debugLevel)::log; + Consumer<String> logConsumer = LOG.atLevel(Level.INFO)::log; processWrapperFactory.create(module.getWorkDir(), logConsumer, logConsumer, envProperties, args.toArray(new String[0])).execute(); LOG.info("Generated manifests zip file: {}", zipName); success = true; @@ -107,4 +121,81 @@ public class CliService { telemetryCache.put("scanner.sca.execution.cli.success", String.valueOf(success)); } } + + private @Nullable String getExcludeFlag(DefaultInputModule module, DefaultConfiguration configuration) throws IOException { + List<String> configExcludedPaths = getConfigExcludedPaths(configuration); + List<String> scmIgnoredPaths = getScmIgnoredPaths(module); + + ArrayList<String> mergedExclusionPaths = new ArrayList<>(); + mergedExclusionPaths.addAll(configExcludedPaths); + mergedExclusionPaths.addAll(scmIgnoredPaths); + + String workDirExcludedPath = getWorkDirExcludedPath(module); + if (workDirExcludedPath != null) { + mergedExclusionPaths.add(workDirExcludedPath); + } + + if (mergedExclusionPaths.isEmpty()) { + return null; + } + + // wrap each exclusion path in quotes to handle commas in file paths + return toCsvString(mergedExclusionPaths); + } + + private static List<String> getConfigExcludedPaths(DefaultConfiguration configuration) { + String[] excludedPaths = configuration.getStringArray(EXCLUDED_MANIFESTS_PROP_KEY); + if (excludedPaths == null) { + return List.of(); + } + return Arrays.stream(excludedPaths).toList(); + } + + private List<String> getScmIgnoredPaths(DefaultInputModule module) { + var scmProvider = scmConfiguration.provider(); + // Only Git is supported at this time + if (scmProvider == null || scmProvider.key() == null || !scmProvider.key().equals("git")) { + return List.of(); + } + + if (scmConfiguration.isExclusionDisabled()) { + // The user has opted out of using the SCM exclusion rules + return List.of(); + } + + Path baseDirPath = module.getBaseDir(); + List<String> scmIgnoredPaths = JGitUtils.getAllIgnoredPaths(baseDirPath); + if (scmIgnoredPaths.isEmpty()) { + return List.of(); + } + return scmIgnoredPaths.stream() + .map(ignoredPathRel -> { + boolean isDirectory = Files.isDirectory(baseDirPath.resolve(ignoredPathRel)); + // Directories need to get turned into a glob for the Tidelift CLI + return isDirectory ? (ignoredPathRel + "/**") : ignoredPathRel; + }) + .toList(); + } + + private static String getWorkDirExcludedPath(DefaultInputModule module) { + Path baseDir = module.getBaseDir().toAbsolutePath().normalize(); + Path workDir = module.getWorkDir().toAbsolutePath().normalize(); + + if (workDir.startsWith(baseDir)) { + // workDir is inside baseDir, so return the relative path as a glob + Path relativeWorkDir = baseDir.relativize(workDir); + return relativeWorkDir + "/**"; + } + + return null; + } + + private static String toCsvString(List<String> values) throws IOException { + StringWriter sw = new StringWriter(); + try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { + printer.printRecord(values); + } + // trim to remove the trailing newline + return sw.toString().trim(); + } } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java index 26a98e65fdc..5c848b4ddbc 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sca/ScaProperties.java @@ -20,7 +20,9 @@ package org.sonar.scanner.sca; import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.sonar.scanner.config.DefaultConfiguration; @@ -44,14 +46,22 @@ public class ScaProperties { * { "sonar.someOtherProperty" : "value" } returns an empty map * * @param configuration the scanner configuration possibly containing sonar.sca.* properties + * @param ignoredPropertyNames property names that should not be processed as a property * @return a map of Tidelift CLI compatible environment variable names to their configuration values */ - public static Map<String, String> buildFromScannerProperties(DefaultConfiguration configuration) { - return configuration - .getProperties() + public static Map<String, String> buildFromScannerProperties(DefaultConfiguration configuration, Set<String> ignoredPropertyNames) { + HashMap<String, String> props = new HashMap<>(configuration.getProperties()); + + // recursive mode defaults to true + if (!props.containsKey("sonar.sca.recursiveManifestSearch")) { + props.put("sonar.sca.recursiveManifestSearch", "true"); + } + + return props .entrySet() .stream() .filter(entry -> entry.getKey().startsWith(SONAR_SCA_PREFIX)) + .filter(entry -> !ignoredPropertyNames.contains(entry.getKey())) .collect(Collectors.toMap(entry -> convertPropToEnvVariable(entry.getKey()), Map.Entry::getValue)); } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java index 855fd945109..6fb38fa4563 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/sensor/ProjectSensorContext.java @@ -260,4 +260,5 @@ public class ProjectSensorContext implements SensorContext { } return false; } + } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java index 972a8ce8da3..5b3b3257142 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scm/git/JGitUtils.java @@ -21,6 +21,9 @@ package org.sonar.scm.git; import java.io.IOException; import java.nio.file.Path; +import java.util.List; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.Repository; @@ -41,4 +44,15 @@ public class JGitUtils { throw new IllegalStateException("Unable to open Git repository", e); } } + + // Return a list of scm ignored paths relative to the baseDir. + public static List<String> getAllIgnoredPaths(Path baseDir) { + try (Repository repo = buildRepository(baseDir)) { + try (Git git = new Git(repo)) { + return git.status().call().getIgnoredNotInIndex().stream().sorted().toList(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + } } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java index 5a124b74214..de4a41c519b 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/issue/IssuePublisherTest.java @@ -26,6 +26,8 @@ import java.util.List; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -67,7 +69,6 @@ import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; @RunWith(MockitoJUnitRunner.class) public class IssuePublisherTest { private static final RuleKey JAVA_RULE_KEY = RuleKey.of("java", "AvoidCycle"); - private static final RuleKey NOSONAR_RULE_KEY = RuleKey.of("java", "NoSonarCheck"); private DefaultInputProject project; @@ -279,11 +280,13 @@ public class IssuePublisherTest { verifyNoInteractions(reportPublisher); } - @Test - public void should_accept_issues_on_no_sonar_rules() { + @ParameterizedTest + @ValueSource(strings = {"NoSonarCheck", "S1291", "S1291Check"}) + public void should_accept_issues_on_no_sonar_rules(String noSonarRule) { + RuleKey noSonarRuleKey = RuleKey.of("java", noSonarRule); // The "No Sonar" rule logs violations on the lines that are flagged with "NOSONAR" !! activeRulesBuilder.addRule(new NewActiveRule.Builder() - .setRuleKey(NOSONAR_RULE_KEY) + .setRuleKey(noSonarRuleKey) .setSeverity(Severity.INFO) .setQProfileKey("qp-1") .build()); @@ -293,7 +296,7 @@ public class IssuePublisherTest { DefaultIssue issue = new DefaultIssue(project) .at(new DefaultIssueLocation().on(file).at(file.selectLine(3)).message("")) - .forRule(NOSONAR_RULE_KEY); + .forRule(noSonarRuleKey); when(filters.accept(any(InputComponent.class), any(ScannerReport.Issue.class), anyString())).thenReturn(true); diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java index b8bb1a26961..597fafa833c 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/CliServiceTest.java @@ -23,25 +23,34 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.net.URL; +import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.SystemUtils; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; import org.sonar.api.batch.bootstrap.ProjectDefinition; import org.sonar.api.batch.fs.internal.DefaultInputModule; +import org.sonar.api.batch.scm.ScmProvider; import org.sonar.api.platform.Server; import org.sonar.api.testfixtures.log.LogTesterJUnit5; import org.sonar.api.utils.System2; import org.sonar.core.util.ProcessWrapperFactory; import org.sonar.scanner.config.DefaultConfiguration; import org.sonar.scanner.repository.TelemetryCache; +import org.sonar.scanner.scm.ScmConfiguration; +import org.sonar.scm.git.GitScmProvider; +import org.sonar.scm.git.JGitUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.slf4j.event.Level.DEBUG; @@ -55,25 +64,53 @@ class CliServiceTest { private final LogTesterJUnit5 logTester = new LogTesterJUnit5(); @TempDir Path rootModuleDir; + private final ScmConfiguration scmConfiguration = mock(ScmConfiguration.class); + private final ScmProvider scmProvider = mock(GitScmProvider.class); + ProcessWrapperFactory processWrapperFactory = mock(ProcessWrapperFactory.class, CALLS_REAL_METHODS); + private MockedStatic<JGitUtils> jGitUtilsMock; + DefaultConfiguration configuration = mock(DefaultConfiguration.class); private CliService underTest; @BeforeEach - void setup() { + void setup() throws IOException { telemetryCache = new TelemetryCache(); + Path workDir = rootModuleDir.resolve(".scannerwork"); + Files.createDirectories(workDir); rootInputModule = new DefaultInputModule( - ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(rootModuleDir.toFile())); - underTest = new CliService(new ProcessWrapperFactory(), telemetryCache, System2.INSTANCE, server); + ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(workDir.toFile())); + when(scmConfiguration.provider()).thenReturn(scmProvider); + when(scmProvider.key()).thenReturn("git"); + when(scmConfiguration.isExclusionDisabled()).thenReturn(false); + jGitUtilsMock = org.mockito.Mockito.mockStatic(JGitUtils.class); + jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of("ignored.txt")); + when(server.getVersion()).thenReturn("1.0.0"); + logTester.setLevel(INFO); + when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true)); + + underTest = new CliService(processWrapperFactory, telemetryCache, System2.INSTANCE, server, scmConfiguration); + } + + @AfterEach + void teardown() { + if (jGitUtilsMock != null) { + jGitUtilsMock.close(); + } } @Test void generateZip_shouldCallProcessCorrectly_andRegisterTelemetry() throws IOException, URISyntaxException { assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue(); - // We need to set the logging level to debug in order to be able to view the shell script's output - logTester.setLevel(DEBUG); + when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true", CliService.EXCLUDED_MANIFESTS_PROP_KEY, "foo,bar,baz/**")); + when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true")); + when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"foo", "bar", "baz/**"}); - List<String> args = List.of( + File producedZip = underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + assertThat(producedZip).exists(); + + var expectedArguments = List.of( "projects", "save-lockfiles", "--zip", @@ -81,35 +118,31 @@ class CliServiceTest { rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(), "--directory", rootInputModule.getBaseDir().toString(), + "--exclude", + "foo,bar,baz/**,ignored.txt,.scannerwork/**", "--debug"); - String argumentOutput = "Arguments Passed In: " + String.join(" ", args); - DefaultConfiguration configuration = mock(DefaultConfiguration.class); - when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true")); - when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true")); - - File producedZip = underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - assertThat(producedZip).exists(); - - assertThat(logTester.logs(DEBUG)) - .contains(argumentOutput) + assertThat(logTester.logs(INFO)) + .contains("Arguments Passed In: " + String.join(" ", expectedArguments)) .contains("TIDELIFT_SKIP_UPDATE_CHECK=1") .contains("TIDELIFT_ALLOW_MANIFEST_FAILURES=1") - .contains("TIDELIFT_RECURSIVE_MANIFEST_SEARCH=true"); - assertThat(logTester.logs(INFO)).contains("Generated manifests zip file: " + producedZip.getName()); + .contains("TIDELIFT_RECURSIVE_MANIFEST_SEARCH=true") + .contains("Generated manifests zip file: " + producedZip.getName()); assertThat(telemetryCache.getAll()).containsKey("scanner.sca.execution.cli.duration").isNotNull(); assertThat(telemetryCache.getAll()).containsEntry("scanner.sca.execution.cli.success", "true"); } @Test - void generateZip_whenDebugLogLevel_shouldCallProcessCorrectly() throws IOException, URISyntaxException { + void generateZip_whenDebugLogLevelAndScaDebugNotEnabled_shouldWriteDebugLogsToDebugStream() throws IOException, URISyntaxException { + logTester.setLevel(DEBUG); + when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(false)); + assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue(); - // We need to set the logging level to debug in order to be able to view the shell script's output - logTester.setLevel(DEBUG); + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - List<String> args = List.of( + var expectedArguments = List.of( "projects", "save-lockfiles", "--zip", @@ -117,27 +150,52 @@ class CliServiceTest { rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(), "--directory", rootInputModule.getBaseDir().toString(), + "--exclude", + "ignored.txt,.scannerwork/**", "--debug"); - String argumentOutput = "Arguments Passed In: " + String.join(" ", args); - DefaultConfiguration configuration = mock(DefaultConfiguration.class); - when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true")); - when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true")); + assertThat(logTester.logs(INFO)) + .contains("Arguments Passed In: " + String.join(" ", expectedArguments)); + } + + @Test + void generateZip_whenScaDebugEnabled_shouldWriteDebugLogsToInfoStream() throws IOException, URISyntaxException { + when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true)); + + assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue(); underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - assertThat(logTester.logs(DEBUG)) - .contains(argumentOutput); + var expectedArguments = List.of( + "projects", + "save-lockfiles", + "--zip", + "--zip-filename", + rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(), + "--directory", + rootInputModule.getBaseDir().toString(), + "--exclude", + "ignored.txt,.scannerwork/**", + "--debug"); + + assertThat(logTester.logs(INFO)) + .contains("Arguments Passed In: " + String.join(" ", expectedArguments)); } @Test - void generateZip_whenScaDebugEnabled_shouldCallProcessCorrectly() throws IOException, URISyntaxException { - assertThat(rootModuleDir.resolve("test_file").toFile().createNewFile()).isTrue(); + void generateZip_shouldSendSQEnvVars() throws IOException, URISyntaxException { + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - // Set the logging level to info so that we don't automatically set --debug flag - logTester.setLevel(INFO); + assertThat(logTester.logs(INFO)) + .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1") + .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=1.0.0"); + } - List<String> args = List.of( + @Test + void generateZip_includesIgnoredPathsFromGitProvider() throws Exception { + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + var expectedArguments = List.of( "projects", "save-lockfiles", "--zip", @@ -145,34 +203,128 @@ class CliServiceTest { rootInputModule.getWorkDir().resolve("dependency-files.zip").toString(), "--directory", rootInputModule.getBaseDir().toString(), + "--exclude", + "ignored.txt,.scannerwork/**", "--debug"); - String argumentOutput = "Arguments Passed In: " + String.join(" ", args); - DefaultConfiguration configuration = mock(DefaultConfiguration.class); - when(configuration.getProperties()).thenReturn(Map.of("sonar.sca.recursiveManifestSearch", "true")); - when(configuration.get("sonar.sca.recursiveManifestSearch")).thenReturn(Optional.of("true")); - when(configuration.getBoolean("sonar.sca.debug")).thenReturn(Optional.of(true)); + assertThat(logTester.logs(INFO)) + .contains("Arguments Passed In: " + String.join(" ", expectedArguments)) + .contains("TIDELIFT_SKIP_UPDATE_CHECK=1") + .contains("TIDELIFT_ALLOW_MANIFEST_FAILURES=1") + .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1") + .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=1.0.0"); + + } + + @Test + void generateZip_withNoScm_doesNotIncludeScmIgnoredPaths() throws Exception { + when(scmConfiguration.provider()).thenReturn(null); underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - assertThat(logTester.logs(INFO)) - .contains(argumentOutput); + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug"); } @Test - void generateZip_shouldSendSQEnvVars() throws IOException, URISyntaxException { - // We need to set the logging level to debug in order to be able to view the shell script's output - logTester.setLevel(DEBUG); + void generateZip_withNonGit_doesNotIncludeScmIgnoredPaths() throws Exception { + when(scmProvider.key()).thenReturn("notgit"); + + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug"); + } - var version = "1.0.0"; - when(server.getVersion()).thenReturn(version); + @Test + void generateZip_withExclusionDisabled_doesNotIncludeScmIgnoredPaths() throws Exception { + when(scmConfiguration.isExclusionDisabled()).thenReturn(true); - DefaultConfiguration configuration = mock(DefaultConfiguration.class); underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); - assertThat(logTester.logs(DEBUG)) - .contains("TIDELIFT_CLI_INSIDE_SCANNER_ENGINE=1") - .contains("TIDELIFT_CLI_SQ_SERVER_VERSION=" + version); + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug"); + } + + @Test + void generateZip_withNoScmIgnores_doesNotIncludeScmIgnoredPaths() throws Exception { + jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of()); + + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude .scannerwork/** --debug"); + } + + @Test + void generateZip_withExistingExcludedManifests_appendsScmIgnoredPaths() throws Exception { + when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] {"**/test/**"}); + + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude **/test/**,ignored.txt,.scannerwork/**"); + } + + @Test + void generateZip_withExcludedManifestsSettingContainingBadCharacters_handlesTheBadCharacters() throws Exception { + when(configuration.getStringArray(CliService.EXCLUDED_MANIFESTS_PROP_KEY)).thenReturn(new String[] { + "**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**"}); + + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + + String expectedExcludeFlag = """ + --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**",ignored.txt + """.strip(); + assertThat(capturedArgs).contains(expectedExcludeFlag); + } + + @Test + void generateZip_withScmIgnoresContainingBadCharacters_handlesTheBadCharacters() throws Exception { + jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))) + .thenReturn(List.of("**/test/**", "**/path with spaces/**", "**/path,with,commas/**", "**/path'with'quotes/**", "**/path\"with\"double\"quotes/**")); + + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + + String expectedExcludeFlag = """ + --exclude **/test/**,**/path with spaces/**,"**/path,with,commas/**",**/path'with'quotes/**,"**/path""with""double""quotes/**" + """.strip(); + assertThat(capturedArgs).contains(expectedExcludeFlag); + } + + @Test + void generateZip_withIgnoredDirectories_GlobifiesDirectories() throws Exception { + String ignoredDirectory = "directory1"; + Files.createDirectories(rootModuleDir.resolve(ignoredDirectory)); + String ignoredFile = "directory2/file.txt"; + Path ignoredFilePath = rootModuleDir.resolve(ignoredFile); + Files.createDirectories(ignoredFilePath.getParent()); + Files.createFile(ignoredFilePath); + + jGitUtilsMock.when(() -> JGitUtils.getAllIgnoredPaths(any(Path.class))).thenReturn(List.of(ignoredDirectory, ignoredFile)); + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + assertThat(capturedArgs).contains("--exclude directory1/**,directory2/file.txt"); + } + + @Test + void generateZip_withExternalWorkDir_DoesNotExcludeWorkingDir() throws URISyntaxException, IOException { + Path externalWorkDir = Files.createTempDirectory("externalWorkDir"); + try { + rootInputModule = new DefaultInputModule(ProjectDefinition.create().setBaseDir(rootModuleDir.toFile()).setWorkDir(externalWorkDir.toFile())); + underTest.generateManifestsZip(rootInputModule, scriptDir(), configuration); + String capturedArgs = logTester.logs().stream().filter(log -> log.contains("Arguments Passed In:")).findFirst().get(); + + // externalWorkDir is not present in the exclude flag + assertThat(capturedArgs).contains("--exclude ignored.txt --debug"); + } finally { + externalWorkDir.toFile().delete(); + } } private URL scriptUrl() { diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java index c19600dcf3c..e598a225b9c 100644 --- a/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java +++ b/sonar-scanner-engine/src/test/java/org/sonar/scanner/sca/ScaPropertiesTest.java @@ -19,9 +19,11 @@ */ package org.sonar.scanner.sca; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; import org.sonar.scanner.config.DefaultConfiguration; @@ -34,34 +36,36 @@ class ScaPropertiesTest { private final DefaultConfiguration configuration = mock(DefaultConfiguration.class); @Test - void buildFromScannerProperties_shouldReturnEmptyMap_whenNoPropertiesExist() { + void buildFromScannerProperties_withNoProperties_returnsDefaultMap() { when(configuration.get(anyString())).thenReturn(Optional.empty()); - var result = ScaProperties.buildFromScannerProperties(configuration); + var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet()); - assertThat(result).isEmpty(); + assertThat(result).containsExactly( + Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true")); } @Test - void buildFromScannerProperties_shouldIgnoresUnmappedProperties() { + void buildFromScannerProperties_withUnmappedProperties_ignoresProperties() { var inputProperties = new HashMap<String, String>(); inputProperties.put("sonar.sca.pythonBinary", "/usr/bin/python3"); inputProperties.put("sonar.sca.unknownProperty", "value"); - inputProperties.put("sonar.somethingElse", "ignoreMe"); + inputProperties.put("sonar.somethingElse", "dont-include-non-sca"); + inputProperties.put("sonar.sca.ignoredProperty", "ignore-me"); when(configuration.getProperties()).thenReturn(inputProperties); when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class)))); - var result = ScaProperties.buildFromScannerProperties(configuration); + var result = ScaProperties.buildFromScannerProperties(configuration, Set.of("sonar.sca.ignoredProperty")); assertThat(result).containsExactly( + Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true"), Map.entry("TIDELIFT_PYTHON_BINARY", "/usr/bin/python3"), Map.entry("TIDELIFT_UNKNOWN_PROPERTY", "value")); } @Test - void buildFromScannerProperties_shouldMapAllKnownProperties() { + void buildFromScannerProperties_withLotsOfProperties_mapsAllProperties() { var inputProperties = new HashMap<String, String>(); - inputProperties.put("sonar.sca.excludedManifests", "exclude/*"); inputProperties.put("sonar.sca.goNoResolve", "true"); inputProperties.put("sonar.sca.gradleConfigurationPattern", "pattern"); inputProperties.put("sonar.sca.gradleNoResolve", "false"); @@ -80,7 +84,6 @@ class ScaPropertiesTest { when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class)))); var expectedProperties = new HashMap<String, String>(); - expectedProperties.put("TIDELIFT_EXCLUDED_MANIFESTS", "exclude/*"); expectedProperties.put("TIDELIFT_GO_NO_RESOLVE", "true"); expectedProperties.put("TIDELIFT_GRADLE_CONFIGURATION_PATTERN", "pattern"); expectedProperties.put("TIDELIFT_GRADLE_NO_RESOLVE", "false"); @@ -96,8 +99,35 @@ class ScaPropertiesTest { expectedProperties.put("TIDELIFT_PYTHON_RESOLVE_LOCAL", "false"); expectedProperties.put("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true"); - var result = ScaProperties.buildFromScannerProperties(configuration); + var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet()); assertThat(result).containsExactlyInAnyOrderEntriesOf(expectedProperties); } + + + @Test + void buildFromScannerProperties_withoutRecursiveModeProp_defaultsRecursiveModeTrue() { + var inputProperties = new HashMap<String, String>(); + when(configuration.getProperties()).thenReturn(inputProperties); + when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class)))); + + var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet()); + + assertThat(result).containsExactly( + Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "true")); + } + + @Test + void buildFromScannerProperties_withRecursiveModeProp_usesPropAsOverride() { + var inputProperties = new HashMap<String, String>(); + inputProperties.put("sonar.sca.recursiveManifestSearch", "false"); + when(configuration.getProperties()).thenReturn(inputProperties); + when(configuration.get(anyString())).thenAnswer(i -> Optional.ofNullable(inputProperties.get(i.getArgument(0, String.class)))); + + var result = ScaProperties.buildFromScannerProperties(configuration, Collections.emptySet()); + + assertThat(result).containsExactly( + Map.entry("TIDELIFT_RECURSIVE_MANIFEST_SEARCH", "false")); + } + } diff --git a/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java new file mode 100644 index 00000000000..3578a3c60f7 --- /dev/null +++ b/sonar-scanner-engine/src/test/java/org/sonar/scm/git/JGitUtilsTest.java @@ -0,0 +1,67 @@ +/* + * SonarQube + * Copyright (C) 2009-2025 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.scm.git; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.eclipse.jgit.api.Git; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.sonar.api.utils.MessageException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JGitUtilsTest { + + @TempDir + Path rootModuleDir; + + @Test + void getAllIgnoredPaths_ReturnsIgnoredFiles() throws Exception { + Git.init().setDirectory(rootModuleDir.toFile()).call(); + Files.createDirectories(rootModuleDir.resolve("directory1")); + Files.createDirectories(rootModuleDir.resolve("directory2")); + Files.createDirectories(rootModuleDir.resolve("directory3")); + Files.write(rootModuleDir.resolve("directory1/file_a.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve("directory1/file_b.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve("directory2/file_a.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve("directory2/file_b.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve("directory3/file_a.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve("directory3/file_b.txt"), "content".getBytes()); + Files.write(rootModuleDir.resolve(".gitignore"), "ignored.txt\ndirectory1\ndirectory2/file_a.txt".getBytes()); + Files.write(rootModuleDir.resolve("directory3/.gitignore"), "file_b.txt".getBytes()); + + List<String> result = JGitUtils.getAllIgnoredPaths(rootModuleDir); + + // in directory1, the entire directory is ignored without listing each file + // in directory2, specific files are ignored, so those files are listed + // in directory3, specific files are ignored via a separate .gitignore file + assertThat(result).isEqualTo(List.of("directory1", "directory2/file_a.txt", "directory3/file_b.txt")); + } + + @Test + void getIgnoredPaths_WithNonGitDirectory_ThrowsException() { + assertThatThrownBy(() -> JGitUtils.getAllIgnoredPaths(rootModuleDir)) + .isInstanceOf(MessageException.class) + .hasMessageStartingWith("Not inside a Git work tree: "); + } +} diff --git a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto index 2cc8bba8afb..ce41bae08ad 100644 --- a/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto +++ b/sonar-ws/src/main/protobuf/ws-qualityprofiles.proto @@ -74,14 +74,16 @@ message CreateWsResponse { optional string languageName = 4; optional bool isInherited = 5; optional bool isDefault = 6; - optional Infos infos = 7; - optional Warnings warnings = 8; + optional Infos infos = 7 [deprecated=true]; + optional Warnings warnings = 8 [deprecated=true]; message Infos { + option deprecated = true; repeated string infos = 1; } message Warnings { + option deprecated = true; repeated string warnings = 1; } } diff --git a/sonar-ws/src/testFixtures/java/org/sonarqube/ws/tester/UserTester.java b/sonar-ws/src/testFixtures/java/org/sonarqube/ws/tester/UserTester.java index e0566bc23cb..0585322e48e 100644 --- a/sonar-ws/src/testFixtures/java/org/sonarqube/ws/tester/UserTester.java +++ b/sonar-ws/src/testFixtures/java/org/sonarqube/ws/tester/UserTester.java @@ -129,6 +129,7 @@ public class UserTester { .setLogin(u.getLogin()) .setPermission("admin")); dismissModesTour(u); + dimissDnaTour(u); return u; } @@ -141,6 +142,7 @@ public class UserTester { session.wsClient().permissions().addUser(new org.sonarqube.ws.client.permissions.AddUserRequest().setLogin(user.getLogin()).setPermission("admin")); session.wsClient().userGroups().addUser(new AddUserRequest().setLogin(user.getLogin()).setName("sonar-administrators")); dismissModesTour(user); + dimissDnaTour(user); return user; } @@ -150,6 +152,12 @@ public class UserTester { .credentials(user.getLogin(), user.getLogin()) .build()).users().dismissNotice("showNewModesTour"); } + private void dimissDnaTour(User user) { + WsClientFactories.getDefault().newClient(HttpConnector.newBuilder() + .url(session.wsClient().wsConnector().baseUrl()) + .credentials(user.getLogin(), user.getLogin()) + .build()).users().dismissNotice("showDesignAndArchitectureTour"); + } public UsersService service() { return session.wsClient().users(); |