diff options
author | Jacek <jacek.poreda@sonarsource.com> | 2020-03-30 15:51:39 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2020-04-15 20:03:38 +0000 |
commit | c53ebc67f7c303397bf36c95aab306a0f17f27aa (patch) | |
tree | 06c82a22b5183c776e1049bbc5de3a18ba5fff1b | |
parent | bd73de80860fda06408f7c342ba7455250b16151 (diff) | |
download | sonarqube-c53ebc67f7c303397bf36c95aab306a0f17f27aa.tar.gz sonarqube-c53ebc67f7c303397bf36c95aab306a0f17f27aa.zip |
SONAR-13189 add 'qualifiers' filter and facet to WS
7 files changed, 314 insertions, 19 deletions
diff --git a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java index cf23696d95a..029b9ffbca3 100644 --- a/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ b/server/sonar-webserver-es/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -52,6 +52,7 @@ import org.elasticsearch.search.aggregations.metrics.sum.Sum; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.NestedSortBuilder; import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Qualifiers; import org.sonar.api.server.ServerSide; import org.sonar.api.utils.System2; import org.sonar.core.util.stream.MoreCollectors; @@ -127,6 +128,7 @@ import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_LAST_ANALYSIS_DATE; import static org.sonar.server.measure.index.ProjectMeasuresQuery.SORT_BY_NAME; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUAGES; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_QUALIFIER; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS; import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE; @@ -158,6 +160,7 @@ public class ProjectMeasuresIndex { NEW_SECURITY_HOTSPOTS_REVIEWED(new RangeMeasureFacet(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, SECURITY_REVIEW_RATING_THRESHOLDS)), ALERT_STATUS(new MeasureFacet(ALERT_STATUS_KEY, ProjectMeasuresIndex::buildAlertStatusFacet)), LANGUAGES(FILTER_LANGUAGES, FIELD_LANGUAGES, STICKY, ProjectMeasuresIndex::buildLanguageFacet), + QUALIFIER(FILTER_QUALIFIER, FIELD_QUALIFIER, STICKY, ProjectMeasuresIndex::buildQualifierFacet), TAGS(FILTER_TAGS, FIELD_TAGS, STICKY, ProjectMeasuresIndex::buildTagsFacet); private final String name; @@ -342,6 +345,14 @@ public class ProjectMeasuresIndex { .toArray(KeyedFilter[]::new)); } + private static AbstractAggregationBuilder<?> createQualifierFacet() { + return filters( + FILTER_QUALIFIER, + Stream.of(Qualifiers.APP, Qualifiers.PROJECT) + .map(qualifier -> new KeyedFilter(qualifier, termQuery(FIELD_QUALIFIER, qualifier))) + .toArray(KeyedFilter[]::new)); + } + private AllFilters createFilters(ProjectMeasuresQuery query) { AllFilters filters = RequestFiltersComputer.newAllFilters(); filters.addFilter( @@ -593,4 +604,11 @@ public class ProjectMeasuresIndex { NO_EXTRA_FILTER, extraSubAgg); } + private static FilterAggregationBuilder buildQualifierFacet(Facet facet, ProjectMeasuresQuery query, TopAggregationHelper topAggregationHelper) { + return topAggregationHelper.buildTopAggregation( + facet.getName(), facet.getTopAggregationDef(), + NO_EXTRA_FILTER, + t -> t.subAggregation(createQualifierFacet())); + } + } diff --git a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java index a521daec2c3..f8ce765abf6 100644 --- a/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java +++ b/server/sonar-webserver-es/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java @@ -34,7 +34,6 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; -import org.sonar.api.resources.Qualifiers; import org.sonar.api.utils.System2; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; @@ -73,6 +72,7 @@ import static org.sonar.db.user.UserTesting.newUserDto; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.TYPE_PROJECT_MEASURES; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_QUALIFIER; @RunWith(DataProviderRunner.class) public class ProjectMeasuresIndexTest { @@ -1377,6 +1377,80 @@ public class ProjectMeasuresIndexTest { } @Test + public void facet_qualifier() { + index( + // 2 docs with qualifier APP + newDoc().setQualifier(APP), + newDoc().setQualifier(APP), + // 4 docs with qualifier TRK + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT)); + + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FILTER_QUALIFIER)).getFacets().get(FILTER_QUALIFIER); + + assertThat(result).containsOnly( + entry(APP, 2L), + entry(PROJECT, 4L)); + } + + @Test + public void facet_qualifier_is_sticky() { + index( + // 2 docs with qualifier APP + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualifier(APP), + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualifier(APP), + // 4 docs with qualifier TRK + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualifier(PROJECT), + newDoc(NCLOC, 5000d, COVERAGE, 40d).setQualifier(PROJECT), + newDoc(NCLOC, 12000d, COVERAGE, 50d).setQualifier(PROJECT), + newDoc(NCLOC, 13000d, COVERAGE, 60d).setQualifier(PROJECT)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .setQualifiers(Sets.newHashSet(PROJECT)) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 55d)), + new SearchOptions().addFacets(FILTER_QUALIFIER, NCLOC)).getFacets(); + + // Sticky facet on qualifier does not take into account qualifier filter + assertThat(facets.get(FILTER_QUALIFIER)).containsOnly( + entry(APP, 2L), + entry(PROJECT, 3L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 1L), + entry("10000.0-100000.0", 1L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_qualifier_contains_only_app_and_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // 3 docs with qualifier APP, PROJECT + newDoc().setQualifier(APP), + newDoc().setQualifier(APP), + newDoc().setQualifier(PROJECT)); + + // User cannot see these projects + indexForUser(USER2, + // 4 docs with qualifier PROJECT + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT), + newDoc().setQualifier(PROJECT)); + + userSession.logIn(USER1); + LinkedHashMap<String, Long> result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FILTER_QUALIFIER)).getFacets().get(FILTER_QUALIFIER); + + assertThat(result).containsOnly( + entry(APP, 2L), + entry(PROJECT, 1L)); + } + + @Test public void facet_tags() { index( newDoc().setTags(newArrayList("finance", "offshore", "java")), diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java index ebe24c19fad..a804d8bab9d 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactory.java @@ -20,6 +20,7 @@ package org.sonar.server.component.ws; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Sets; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -27,21 +28,24 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; +import java.util.stream.Stream; import javax.annotation.Nullable; import org.sonar.api.measures.Metric.Level; +import org.sonar.api.resources.Qualifiers; import org.sonar.server.component.ws.FilterParser.Criterion; -import org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; import org.sonar.server.measure.index.ProjectMeasuresQuery; +import org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static java.util.Collections.singleton; import static java.util.Locale.ENGLISH; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.IN; -import static org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUAGES; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_QUALIFIER; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS; class ProjectMeasuresQueryFactory { @@ -54,6 +58,7 @@ class ProjectMeasuresQueryFactory { .put(IS_FAVORITE_CRITERION.toLowerCase(ENGLISH), (criterion, query) -> processIsFavorite(criterion)) .put(FILTER_LANGUAGES, ProjectMeasuresQueryFactory::processLanguages) .put(FILTER_TAGS, ProjectMeasuresQueryFactory::processTags) + .put(FILTER_QUALIFIER, ProjectMeasuresQueryFactory::processQualifier) .put(QUERY_KEY, ProjectMeasuresQueryFactory::processQuery) .put(ALERT_STATUS_KEY, ProjectMeasuresQueryFactory::processQualityGateStatus) .build(); @@ -110,6 +115,17 @@ class ProjectMeasuresQueryFactory { throw new IllegalArgumentException("Tags should be set either by using 'tags = java' or 'tags IN (finance, platform)'"); } + private static void processQualifier(Criterion criterion, ProjectMeasuresQuery query) { + checkOperator(criterion); + checkValue(criterion); + Operator operator = criterion.getOperator(); + String value = criterion.getValue(); + checkArgument(EQ.equals(operator), "Only equals operator is available for qualifier criteria"); + String qualifier = Stream.of(Qualifiers.APP, Qualifiers.PROJECT).filter(q -> q.equalsIgnoreCase(value)).findFirst() + .orElseThrow(() -> new IllegalArgumentException(format("Unknown qualifier : '%s'", value))); + query.setQualifiers(Sets.newHashSet(qualifier)); + } + private static void processQuery(Criterion criterion, ProjectMeasuresQuery query) { checkOperator(criterion); Operator operatorValue = criterion.getOperator(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java index e1648e9bdf9..7874d771e9b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/component/ws/SearchProjectsAction.java @@ -135,6 +135,7 @@ public class SearchProjectsAction implements ComponentsWsAction { .addPagingParams(DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE) .setInternal(true) .setChangelog( + new Change("8.3", "Add 'qualifier' filter and facet"), new Change("8.0", "Field 'id' from response has been removed")) .setResponseExample(getClass().getResource("search_projects-example.json")) .setHandler(this); @@ -159,49 +160,55 @@ public class SearchProjectsAction implements ComponentsWsAction { .setDescription("Filter of projects on name, key, measure value, quality gate, language, tag or whether a project is a favorite or not.<br>" + "The filter must be encoded to form a valid URL (for example '=' must be replaced by '%3D').<br>" + "Examples of use:" + - "<ul>" + + HTML_UL_START_TAG + " <li>to filter my favorite projects with a failed quality gate and a coverage greater than or equals to 60% and a coverage strictly lower than 80%:<br>" + " <code>filter=\"alert_status = ERROR and isFavorite and coverage >= 60 and coverage < 80\"</code></li>" + " <li>to filter projects with a reliability, security and maintainability rating equals or worse than B:<br>" + " <code>filter=\"reliability_rating>=2 and security_rating>=2 and sqale_rating>=2\"</code></li>" + " <li>to filter projects without duplication data:<br>" + " <code>filter=\"duplicated_lines_density = NO_DATA\"</code></li>" + - "</ul>" + + HTML_UL_END_TAG + "To filter on project name or key, use the 'query' keyword, for instance : <code>filter='query = \"Sonar\"'</code>.<br>" + "<br>" + "To filter on a numeric metric, provide the metric key.<br>" + "These are the supported metric keys:<br>" + - "<ul>" + + HTML_UL_START_TAG + METRIC_KEYS.stream().sorted().map(key -> "<li>" + key + "</li>").collect(Collectors.joining()) + - "</ul>" + + HTML_UL_END_TAG + "<br>" + "To filter on a rating, provide the corresponding metric key (ex: reliability_rating for reliability rating).<br>" + "The possible values are:" + - "<ul>" + + HTML_UL_START_TAG + " <li>'1' for rating A</li>" + " <li>'2' for rating B</li>" + " <li>'3' for rating C</li>" + " <li>'4' for rating D</li>" + " <li>'5' for rating E</li>" + - "</ul>" + + HTML_UL_END_TAG + "To filter on a Quality Gate status use the metric key 'alert_status'. Only the '=' operator can be used.<br>" + "The possible values are:" + - "<ul>" + + HTML_UL_START_TAG + " <li>'OK' for Passed</li>" + " <li>'WARN' for Warning</li>" + " <li>'ERROR' for Failed</li>" + - "</ul>" + + HTML_UL_END_TAG + "To filter on language keys use the language key: " + - "<ul>" + + HTML_UL_START_TAG + " <li>to filter on a single language you can use 'language = java'</li>" + " <li>to filter on several languages you must use 'language IN (java, js)'</li>" + - "</ul>" + + HTML_UL_END_TAG + "Use the WS api/languages/list to find the key of a language.<br> " + "To filter on tags use the 'tag' keyword:" + - "<ul> " + + HTML_UL_START_TAG + " <li>to filter on one tag you can use <code>tag = finance</code></li>" + " <li>to filter on several tags you must use <code>tag in (offshore, java)</code></li>" + - "</ul>"); + HTML_UL_END_TAG + + "To filter on a qualifier use key 'qualifier'. Only the '=' operator can be used.<br>" + + "The possible values are:" + + HTML_UL_START_TAG + + " <li>TRK - for projects</li>" + + " <li>APP - for applications</li>" + + HTML_UL_END_TAG); action.createParam(Param.SORT) .setDescription("Sort projects by numeric metric key, quality gate status (using '%s'), last analysis date (using '%s'), or by project name.", ALERT_STATUS_KEY, SORT_BY_LAST_ANALYSIS_DATE, PARAM_FILTER) @@ -294,6 +301,20 @@ public class SearchProjectsAction implements ComponentsWsAction { } } + private void filterQualifiersBasedOnEdition(ProjectMeasuresQuery query) { + Set<String> availableQualifiers = getQualifiersFromEdition(); + Set<String> requestQualifiers = query.getQualifiers().orElse(availableQualifiers); + + Set<String> resolvedQualifiers = requestQualifiers.stream() + .filter(availableQualifiers::contains) + .collect(Collectors.toSet()); + if (!resolvedQualifiers.isEmpty()) { + query.setQualifiers(resolvedQualifiers); + } else { + throw new IllegalArgumentException("Invalid qualifier, available are: " + String.join(",", availableQualifiers)); + } + } + private Set<String> getQualifiersFromEdition() { Optional<Edition> edition = editionProvider.get(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java index 797ca5d9f35..1c97e1c4d37 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/ProjectMeasuresQueryFactoryTest.java @@ -35,13 +35,13 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.tuple; +import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.GT; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.IN; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LT; import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LTE; -import static org.sonar.server.component.ws.ProjectMeasuresQueryFactory.newProjectMeasuresQuery; public class ProjectMeasuresQueryFactoryTest { @@ -117,6 +117,30 @@ public class ProjectMeasuresQueryFactoryTest { } @Test + public void create_query_on_qualifier() { + ProjectMeasuresQuery query = newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("qualifier").setOperator(EQ).setValue("APP").build()), + emptySet()); + + assertThat(query.getQualifiers().get()).containsOnly("APP"); + } + + @Test + public void fail_to_create_query_on_qualifier_when_operator_is_not_equal() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Only equals operator is available for qualifier criteria"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("qualifier").setOperator(GT).setValue("APP").build()), emptySet()); + } + + @Test + public void fail_to_create_query_on_qualifier_when_value_is_incorrect() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Unknown qualifier : 'unknown'"); + + newProjectMeasuresQuery(singletonList(Criterion.builder().setKey("qualifier").setOperator(EQ).setValue("unknown").build()), emptySet()); + } + + @Test public void create_query_on_language_using_in_operator() { ProjectMeasuresQuery query = newProjectMeasuresQuery( singletonList(Criterion.builder().setKey("languages").setOperator(IN).setValues(asList("java", "js")).build()), diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index 0f19d36630c..a8b00a512b8 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -70,6 +70,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.tuple; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -102,6 +103,7 @@ import static org.sonar.test.JsonAssert.assertJson; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_FILTER; import static org.sonarqube.ws.client.component.ComponentsWsParameters.PARAM_ORGANIZATION; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_LANGUAGES; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_QUALIFIER; import static org.sonarqube.ws.client.project.ProjectsWsParameters.FILTER_TAGS; @RunWith(DataProviderRunner.class) @@ -144,6 +146,22 @@ public class SearchProjectsActionTest { }; } + @DataProvider + public static Object[][] community_or_developer_edition() { + return new Object[][] { + {Edition.COMMUNITY}, + {Edition.DEVELOPER}, + }; + } + + @DataProvider + public static Object[][] enterprise_or_datacenter_edition() { + return new Object[][] { + {Edition.ENTERPRISE}, + {Edition.DATACENTER}, + }; + } + private DbClient dbClient = db.getDbClient(); private DbSession dbSession = db.getSession(); @@ -172,7 +190,7 @@ public class SearchProjectsActionTest { assertThat(def.isPost()).isFalse(); assertThat(def.responseExampleAsString()).isNotEmpty(); assertThat(def.params().stream().map(Param::key).collect(toList())).containsOnly("organization", "filter", "facets", "s", "asc", "ps", "p", "f"); - assertThat(def.changelog()).hasSize(1); + assertThat(def.changelog()).hasSize(2); Param organization = def.param("organization"); assertThat(organization.isRequired()).isFalse(); @@ -215,7 +233,7 @@ public class SearchProjectsActionTest { Param facets = def.param("facets"); assertThat(facets.defaultValue()).isNull(); assertThat(facets.possibleValues()).containsOnly("ncloc", "duplicated_lines_density", "coverage", "sqale_rating", "reliability_rating", "security_rating", "alert_status", - "languages", "tags", "new_reliability_rating", "new_security_rating", "new_maintainability_rating", "new_coverage", "new_duplicated_lines_density", "new_lines", + "languages", "tags", "qualifier", "new_reliability_rating", "new_security_rating", "new_maintainability_rating", "new_coverage", "new_duplicated_lines_density", "new_lines", "security_review_rating", "security_hotspots_reviewed", "new_security_hotspots_reviewed", "new_security_review_rating"); } @@ -620,7 +638,7 @@ public class SearchProjectsActionTest { @Test @UseDataProvider("component_qualifiers_for_valid_editions") - public void filter_projects_and_apps_by_editions(String[] qualifiers, Edition edition) { + public void default_filter_projects_and_apps_by_editions(String[] qualifiers, Edition edition) { when(editionProviderMock.get()).thenReturn(Optional.of(edition)); userSession.logIn(); OrganizationDto organization = db.organizations().insert(); @@ -676,6 +694,81 @@ public class SearchProjectsActionTest { } @Test + @UseDataProvider("enterprise_or_datacenter_edition") + public void filter_projects_and_apps_by_APP_qualifier_when_ee_dc(Edition edition) { + when(editionProviderMock.get()).thenReturn(Optional.of(edition)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + ComponentDto application1 = insertApplication(organization); + ComponentDto application2 = insertApplication(organization); + ComponentDto application3 = insertApplication(organization); + + insertProject(organization); + insertProject(organization); + insertProject(organization); + + SearchProjectsWsResponse result = call(request.setFilter("qualifier = APP")); + + assertThat(result.getComponentsCount()) + .isEqualTo(3); + + assertThat(result.getComponentsList()).extracting(Component::getKey) + .containsExactly( + Stream.of(application1, application2, application3) + .map(ComponentDto::getDbKey) + .toArray(String[]::new)); + } + + @Test + @UseDataProvider("enterprise_or_datacenter_edition") + public void filter_projects_and_apps_by_TRK_qualifier_when_ee_or_dc(Edition edition) { + when(editionProviderMock.get()).thenReturn(Optional.of(edition)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + + insertApplication(organization); + insertApplication(organization); + insertApplication(organization); + + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto project3 = insertProject(organization); + + SearchProjectsWsResponse result = call(request.setFilter("qualifier = TRK")); + + assertThat(result.getComponentsCount()) + .isEqualTo(3); + + assertThat(result.getComponentsList()).extracting(Component::getKey) + .containsExactly( + Stream.of(project1, project2, project3) + .map(ComponentDto::getDbKey) + .toArray(String[]::new)); + } + + @Test + @UseDataProvider("community_or_developer_edition") + public void fail_when_qualifier_filter_by_APP_set_when_ce_or_de(Edition edition) { + when(editionProviderMock.get()).thenReturn(Optional.of(edition)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + + assertThatThrownBy(() -> call(request.setFilter("qualifiers = APP"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @UseDataProvider("enterprise_or_datacenter_edition") + public void fail_when_qualifier_filter_invalid_when_ee_or_dc(Edition edition) { + when(editionProviderMock.get()).thenReturn(Optional.of(edition)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + + assertThatThrownBy(() -> call(request.setFilter("qualifiers = BLA"))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test public void do_not_return_isFavorite_if_anonymous_user() { userSession.anonymous(); OrganizationDto organization = db.organizations().insert(); @@ -823,6 +916,54 @@ public class SearchProjectsActionTest { } @Test + public void return_qualifiers_facet() { + when(editionProviderMock.get()).thenReturn(Optional.of(Edition.ENTERPRISE)); + userSession.logIn(); + OrganizationDto organization = db.organizations().insert(); + ComponentDto application1 = insertApplication(organization); + ComponentDto application2 = insertApplication(organization); + ComponentDto application3 = insertApplication(organization); + ComponentDto application4 = insertApplication(organization); + + ComponentDto project1 = insertProject(organization); + ComponentDto project2 = insertProject(organization); + ComponentDto project3 = insertProject(organization); + + SearchProjectsWsResponse result = call(request.setFacets(singletonList(FILTER_QUALIFIER))); + + Common.Facet facet = result.getFacets().getFacetsList().stream() + .filter(oneFacet -> FILTER_QUALIFIER.equals(oneFacet.getProperty())) + .findFirst().orElseThrow(IllegalStateException::new); + assertThat(facet.getValuesList()) + .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount) + .containsExactly( + tuple("APP", 4L), + tuple("TRK", 3L)); + } + + @Test + public void return_qualifiers_facet_with_qualifiers_having_no_project_if_qualifiers_is_in_filter() { + when(editionProviderMock.get()).thenReturn(Optional.of(Edition.ENTERPRISE)); + userSession.logIn(); + OrganizationDto organization = db.getDefaultOrganization(); + ComponentDto application1 = insertApplication(organization); + ComponentDto application2 = insertApplication(organization); + ComponentDto application3 = insertApplication(organization); + ComponentDto application4 = insertApplication(organization); + + SearchProjectsWsResponse result = call(request.setFilter("qualifier = APP").setFacets(singletonList(FILTER_QUALIFIER))); + + Common.Facet facet = result.getFacets().getFacetsList().stream() + .filter(oneFacet -> FILTER_QUALIFIER.equals(oneFacet.getProperty())) + .findFirst().orElseThrow(IllegalStateException::new); + assertThat(facet.getValuesList()) + .extracting(Common.FacetValue::getVal, Common.FacetValue::getCount) + .containsExactly( + tuple("APP", 4L), + tuple("TRK", 0L)); + } + + @Test @UseDataProvider("rating_metric_keys") public void return_rating_facet(String ratingMetricKey) { userSession.logIn(); diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java index e3af64ce64d..865abce2a55 100644 --- a/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java +++ b/sonar-ws/src/main/java/org/sonarqube/ws/client/project/ProjectsWsParameters.java @@ -51,6 +51,7 @@ public class ProjectsWsParameters { public static final String FILTER_LANGUAGES = "languages"; public static final String FILTER_TAGS = "tags"; + public static final String FILTER_QUALIFIER = "qualifier"; private ProjectsWsParameters() { // static utils only |