From: Sébastien Lesaint Date: Fri, 20 Jul 2018 14:22:37 +0000 (+0200) Subject: move UserSession out of sonar-server-common X-Git-Tag: 7.5~747 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7acd9403283a3cc49c56acc1461c595b99448e3a;p=sonarqube.git move UserSession out of sonar-server-common and as a consequence remove any need to have UserSession in Compute Engine --- diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index ecdf7f84cb8..6a70c5d6b79 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -62,7 +62,6 @@ import org.sonar.ce.task.projectanalysis.analysis.ProjectConfigurationFactory; import org.sonar.ce.task.projectanalysis.notification.ReportAnalysisFailureNotificationModule; import org.sonar.ce.taskprocessor.CeProcessingScheduler; import org.sonar.ce.taskprocessor.CeTaskProcessorModule; -import org.sonar.ce.user.CeUserSession; import org.sonar.core.component.DefaultResourceTypes; import org.sonar.core.config.CorePropertyDefinitions; import org.sonar.core.extension.CoreExtensionRepositoryImpl; @@ -96,7 +95,6 @@ import org.sonar.server.extension.CoreExtensionStopper; import org.sonar.server.favorite.FavoriteUpdater; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueStorage; -import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.issue.notification.ChangesOnMyIssueNotificationDispatcher; @@ -111,7 +109,6 @@ import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; import org.sonar.server.l18n.ServerI18n; import org.sonar.server.log.ServerLogging; -import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresIndexer; import org.sonar.server.metric.CoreCustomMetrics; import org.sonar.server.metric.DefaultMetricFinder; @@ -122,7 +119,6 @@ import org.sonar.server.notification.email.EmailNotificationChannel; import org.sonar.server.organization.BillingValidationsProxyImpl; import org.sonar.server.organization.DefaultOrganizationProviderImpl; import org.sonar.server.organization.OrganizationFlagsImpl; -import org.sonar.server.permission.index.PermissionIndexer; import org.sonar.server.platform.DefaultServerUpgradeStatus; import org.sonar.server.platform.OfficialDistribution; import org.sonar.server.platform.ServerFileSystemImpl; @@ -292,9 +288,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { System2.INSTANCE, Clock.systemDefaultZone(), - // user session - CeUserSession.class, - // DB DaoModule.class, ReadOnlyPropertiesDao.class, @@ -307,9 +300,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { // rules/qprofiles RuleIndex.class, - // issues - IssueIndex.class, - new OkHttpClientProvider(), computeEngineStatus, @@ -389,7 +379,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { // measure CoreCustomMetrics.class, DefaultMetricFinder.class, - ProjectMeasuresIndex.class, UserIndexer.class, UserIndex.class, @@ -410,7 +399,6 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { IssueStorage.class, IssueIndexer.class, IssueIteratorFactory.class, - PermissionIndexer.class, IssueFieldsSetter.class, // used in Web Services and CE's DebtCalculator FunctionExecutor.class, // used by IssueWorkflow IssueWorkflow.class, // used in Web Services and CE's DebtCalculator diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/user/CeUserSession.java b/server/sonar-ce/src/main/java/org/sonar/ce/user/CeUserSession.java deleted file mode 100644 index 4dee6ad79fe..00000000000 --- a/server/sonar-ce/src/main/java/org/sonar/ce/user/CeUserSession.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.ce.user; - -import java.util.Collection; -import java.util.List; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.permission.OrganizationPermission; -import org.sonar.db.user.GroupDto; -import org.sonar.server.user.UserSession; - -/** - * Implementation of {@link UserSession} which provide not implementation of any method. - *

- * Any use of {@link UserSession} in the Compute Engine will raise an error. - *

- */ -public class CeUserSession implements UserSession { - - private static final String UOE_MESSAGE = "UserSession must not be used from within the Compute Engine"; - - @Override - public String getLogin() { - throw notImplemented(); - } - - @Override - public String getUuid() { - throw notImplemented(); - } - - @Override - public String getName() { - throw notImplemented(); - } - - @Override - public Integer getUserId() { - throw notImplemented(); - } - - @Override - public Collection getGroups() { - throw notImplemented(); - } - - @Override - public boolean isLoggedIn() { - throw notImplemented(); - } - - @Override - public boolean isRoot() { - throw notImplemented(); - } - - @Override - public UserSession checkIsRoot() { - throw notImplemented(); - } - - @Override - public UserSession checkLoggedIn() { - throw notImplemented(); - } - - @Override - public boolean hasPermission(OrganizationPermission permission, String organizationUuid) { - throw notImplemented(); - } - - @Override - public UserSession checkPermission(OrganizationPermission permission, String organizationUuid) { - throw notImplemented(); - } - - @Override - public boolean hasPermission(OrganizationPermission permission, OrganizationDto organization) { - throw notImplemented(); - } - - @Override - public UserSession checkPermission(OrganizationPermission permission, OrganizationDto organization) { - throw notImplemented(); - } - - @Override - public UserSession checkComponentPermission(String projectPermission, ComponentDto component) { - throw notImplemented(); - } - - @Override - public UserSession checkComponentUuidPermission(String permission, String componentUuid) { - throw notImplemented(); - } - - @Override - public boolean isSystemAdministrator() { - throw notImplemented(); - } - - @Override - public UserSession checkIsSystemAdministrator() { - throw notImplemented(); - } - - @Override - public boolean hasComponentPermission(String permission, ComponentDto component) { - throw notImplemented(); - } - - @Override - public boolean hasComponentUuidPermission(String permission, String componentUuid) { - throw notImplemented(); - } - - @Override - public List keepAuthorizedComponents(String permission, Collection components) { - throw notImplemented(); - } - - @Override - public boolean hasMembership(OrganizationDto organization) { - throw notImplemented(); - } - - @Override - public UserSession checkMembership(OrganizationDto organization) { - throw notImplemented(); - } - - private static RuntimeException notImplemented() { - throw new UnsupportedOperationException(UOE_MESSAGE); - } -} diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/user/package-info.java b/server/sonar-ce/src/main/java/org/sonar/ce/user/package-info.java deleted file mode 100644 index 174e9701c68..00000000000 --- a/server/sonar-ce/src/main/java/org/sonar/ce/user/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.ce.user; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index 9f1328486fa..ff8e310bc7f 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -97,7 +97,7 @@ public class ComputeEngineContainerImplTest { assertThat(picoContainer.getComponentAdapters()) .hasSize( CONTAINER_ITSELF - + 72 // level 4 + + 70 // level 4 + 6 // content of CeConfigurationModule + 4 // content of CeQueueModule + 3 // content of CeHttpModule @@ -120,7 +120,7 @@ public class ComputeEngineContainerImplTest { ); assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize( COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION - + 30 // level 1 + + 27 // level 1 + 55 // content of DaoModule + 3 // content of EsModule + 54 // content of CorePropertyDefinitions diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/user/CeUserSessionTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/user/CeUserSessionTest.java deleted file mode 100644 index 60935e4672b..00000000000 --- a/server/sonar-ce/src/test/java/org/sonar/ce/user/CeUserSessionTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.ce.user; - -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; - -import static org.sonar.test.ExceptionCauseMatcher.hasType; - -@RunWith(DataProviderRunner.class) -public class CeUserSessionTest { - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private CeUserSession underTest = new CeUserSession(); - - @DataProvider - public static Object[][] ceUserSessionPublicMethods() { - List declaredMethods = Arrays.stream(CeUserSession.class.getDeclaredMethods()) - .filter(m -> Modifier.isPublic(m.getModifiers())) - .collect(Collectors.toList()); - Object[][] res = new Object[declaredMethods.size()][1]; - int i = 0; - for (Method declaredMethod : declaredMethods) { - res[i][0] = declaredMethod; - i++; - } - return res; - } - - @Test - @UseDataProvider("ceUserSessionPublicMethods") - public void all_methods_of_CeUserSession_throw_UOE(Method method) throws InvocationTargetException, IllegalAccessException { - int parametersCount = method.getParameterTypes().length; - switch (parametersCount) { - case 2: - expectUOE(); - method.invoke(underTest, null, null); - break; - case 1: - expectUOE(); - method.invoke(underTest, (Object) null); - break; - case 0: - expectUOE(); - method.invoke(underTest); - break; - default: - throw new IllegalArgumentException("Unsupported number of parameters " + parametersCount); - } - } - - private void expectUOE() { - expectedException.expect(InvocationTargetException.class); - expectedException.expectCause( - hasType(UnsupportedOperationException.class) - .andMessage("UserSession must not be used from within the Compute Engine")); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndex.java deleted file mode 100644 index e41f9880224..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndex.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import com.google.common.annotations.VisibleForTesting; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.function.Consumer; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.elasticsearch.action.search.SearchRequestBuilder; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.search.SearchHit; -import org.elasticsearch.search.SearchHits; -import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregator.KeyedFilter; -import org.elasticsearch.search.aggregations.bucket.filters.InternalFilters; -import org.elasticsearch.search.aggregations.bucket.filters.InternalFilters.InternalBucket; -import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; -import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsAggregationBuilder; -import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; -import org.elasticsearch.search.sort.FieldSortBuilder; -import org.elasticsearch.search.sort.ScoreSortBuilder; -import org.elasticsearch.search.sort.SortOrder; -import org.sonar.api.utils.System2; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.SearchIdResult; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.es.textsearch.ComponentTextSearchFeature; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; -import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory; -import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory.ComponentTextSearchQuery; -import org.sonar.server.permission.index.AuthorizationTypeSupport; - -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.elasticsearch.index.query.QueryBuilders.termsQuery; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_LANGUAGE; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_ORGANIZATION_UUID; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_QUALIFIER; -import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_TYPE_COMPONENT; -import static org.sonar.server.component.index.ComponentIndexDefinition.NAME_ANALYZERS; -import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; - -public class ComponentIndex { - - private static final String FILTERS_AGGREGATION_NAME = "filters"; - private static final String DOCS_AGGREGATION_NAME = "docs"; - - private final EsClient client; - private final AuthorizationTypeSupport authorizationTypeSupport; - private final System2 system2; - - public ComponentIndex(EsClient client, AuthorizationTypeSupport authorizationTypeSupport, System2 system2) { - this.client = client; - this.authorizationTypeSupport = authorizationTypeSupport; - this.system2 = system2; - } - - public SearchIdResult search(ComponentQuery query, SearchOptions searchOptions) { - SearchRequestBuilder requestBuilder = client - .prepareSearch(INDEX_TYPE_COMPONENT) - .setFetchSource(false) - .setFrom(searchOptions.getOffset()) - .setSize(searchOptions.getLimit()); - - BoolQueryBuilder esQuery = boolQuery(); - esQuery.filter(authorizationTypeSupport.createQueryFilter()); - setNullable(query.getQuery(), q -> { - ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQuery.builder() - .setQueryText(q) - .setFieldKey(FIELD_KEY) - .setFieldName(FIELD_NAME) - .build(); - esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, ComponentTextSearchFeatureRepertoire.values())); - }); - setEmptiable(query.getQualifiers(), q -> esQuery.must(termsQuery(FIELD_QUALIFIER, q))); - setNullable(query.getLanguage(), l -> esQuery.must(termQuery(FIELD_LANGUAGE, l))); - setNullable(query.getOrganizationUuid(), o -> esQuery.must(termQuery(FIELD_ORGANIZATION_UUID, o))); - requestBuilder.setQuery(esQuery); - requestBuilder.addSort(SORTABLE_ANALYZER.subField(FIELD_NAME), SortOrder.ASC); - - return new SearchIdResult<>(requestBuilder.get(), id -> id, system2.getDefaultTimeZone()); - } - - public ComponentIndexResults searchSuggestions(SuggestionQuery query) { - return searchSuggestions(query, ComponentTextSearchFeatureRepertoire.values()); - } - - @VisibleForTesting - ComponentIndexResults searchSuggestions(SuggestionQuery query, ComponentTextSearchFeature... features) { - Collection qualifiers = query.getQualifiers(); - if (qualifiers.isEmpty()) { - return ComponentIndexResults.newBuilder().build(); - } - - SearchRequestBuilder request = client - .prepareSearch(INDEX_TYPE_COMPONENT) - .setQuery(createQuery(query, features)) - .addAggregation(createAggregation(query)) - - // the search hits are part of the aggregations - .setSize(0); - - SearchResponse response = request.get(); - - return aggregationsToQualifiers(response); - } - - private static HighlightBuilder.Field createHighlighterField() { - HighlightBuilder.Field field = new HighlightBuilder.Field(FIELD_NAME); - field.highlighterType("fvh"); - field.matchedFields( - Stream.concat( - Stream.of(FIELD_NAME), - Arrays - .stream(NAME_ANALYZERS) - .map(a -> a.subField(FIELD_NAME))) - .toArray(String[]::new)); - return field; - } - - private static FiltersAggregationBuilder createAggregation(SuggestionQuery query) { - return AggregationBuilders.filters( - FILTERS_AGGREGATION_NAME, - query.getQualifiers().stream().map(q -> new KeyedFilter(q, termQuery(FIELD_QUALIFIER, q))).toArray(KeyedFilter[]::new)) - .subAggregation(createSubAggregation(query)); - } - - private static TopHitsAggregationBuilder createSubAggregation(SuggestionQuery query) { - return AggregationBuilders.topHits(DOCS_AGGREGATION_NAME) - .highlighter(new HighlightBuilder() - .encoder("html") - .preTags("") - .postTags("") - .field(createHighlighterField())) - .from(query.getSkip()) - .size(query.getLimit()) - .sort(new ScoreSortBuilder()) - .sort(new FieldSortBuilder(ComponentIndexDefinition.FIELD_NAME)) - .fetchSource(false); - } - - private QueryBuilder createQuery(SuggestionQuery query, ComponentTextSearchFeature... features) { - BoolQueryBuilder esQuery = boolQuery(); - esQuery.filter(authorizationTypeSupport.createQueryFilter()); - ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQuery.builder() - .setQueryText(query.getQuery()) - .setFieldKey(FIELD_KEY) - .setFieldName(FIELD_NAME) - .setRecentlyBrowsedKeys(query.getRecentlyBrowsedKeys()) - .setFavoriteKeys(query.getFavoriteKeys()) - .build(); - return esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, features)); - } - - private static ComponentIndexResults aggregationsToQualifiers(SearchResponse response) { - InternalFilters filtersAgg = response.getAggregations().get(FILTERS_AGGREGATION_NAME); - List buckets = filtersAgg.getBuckets(); - return ComponentIndexResults.newBuilder() - .setQualifiers( - buckets.stream().map(ComponentIndex::bucketToQualifier)) - .build(); - } - - private static ComponentHitsPerQualifier bucketToQualifier(InternalBucket bucket) { - InternalTopHits docs = bucket.getAggregations().get(DOCS_AGGREGATION_NAME); - - SearchHits hitList = docs.getHits(); - SearchHit[] hits = hitList.getHits(); - - return new ComponentHitsPerQualifier(bucket.getKey(), ComponentHit.fromSearchHits(hits), hitList.getTotalHits()); - } - - private static void setNullable(@Nullable T parameter, Consumer consumer) { - if (parameter != null) { - consumer.accept(parameter); - } - } - - private static void setEmptiable(Collection parameter, Consumer> consumer) { - if (!parameter.isEmpty()) { - consumer.accept(parameter); - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java deleted file mode 100644 index 9154f3b1247..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static java.util.Collections.emptyList; -import static java.util.Objects.requireNonNull; - -public class ComponentIndexResults { - - private final List qualifiers; - - private ComponentIndexResults(Builder builder) { - this.qualifiers = requireNonNull(builder.qualifiers); - } - - public Stream getQualifiers() { - return qualifiers.stream(); - } - - public boolean isEmpty() { - return qualifiers.isEmpty(); - } - - public static Builder newBuilder() { - return new Builder(); - } - - public static class Builder { - - private List qualifiers = emptyList(); - - private Builder() { - } - - public Builder setQualifiers(Stream qualifiers) { - this.qualifiers = qualifiers.collect(Collectors.toList()); - return this; - } - - public ComponentIndexResults build() { - return new ComponentIndexResults(this); - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentQuery.java b/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentQuery.java deleted file mode 100644 index 809362008ab..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/component/index/ComponentQuery.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Collection; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; - -import static java.util.Collections.emptySet; -import static java.util.Collections.unmodifiableCollection; - -public class ComponentQuery { - private final String organizationUuid; - private final String query; - private final Collection qualifiers; - private final String language; - - private ComponentQuery(Builder builder) { - this.organizationUuid = builder.organizationUuid; - this.query = builder.query; - this.qualifiers = builder.qualifiers; - this.language = builder.language; - } - - @CheckForNull - public String getOrganizationUuid() { - return organizationUuid; - } - - @CheckForNull - public String getQuery() { - return query; - } - - public Collection getQualifiers() { - return qualifiers; - } - - @CheckForNull - public String getLanguage() { - return language; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private String organizationUuid; - private String query; - private Collection qualifiers = emptySet(); - private String language; - - private Builder() { - // enforce static factory method - } - - public Builder setOrganization(@Nullable String organizationUuid) { - this.organizationUuid = organizationUuid; - return this; - } - - public Builder setQuery(@Nullable String query) { - this.query = query; - return this; - } - - public Builder setQualifiers(Collection qualifiers) { - this.qualifiers = unmodifiableCollection(qualifiers); - return this; - } - - public Builder setLanguage(@Nullable String language) { - this.language = language; - return this; - } - - public ComponentQuery build() { - return new ComponentQuery(this); - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsModule.java index bb306dc55fc..a742daeef4c 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/EsModule.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/EsModule.java @@ -20,12 +20,10 @@ package org.sonar.server.es; import org.sonar.core.platform.Module; -import org.sonar.server.permission.index.AuthorizationTypeSupport; public class EsModule extends Module { @Override protected void configureModule() { - add(AuthorizationTypeSupport.class); add(new EsClientProvider()); add(EsClientStopper.class); } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/es/NewIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/es/NewIndex.java index 9365bee84f0..f80b2311635 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/es/NewIndex.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/es/NewIndex.java @@ -32,7 +32,6 @@ import org.apache.commons.lang.StringUtils; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.common.settings.Settings; import org.sonar.api.config.Configuration; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; @@ -53,6 +52,10 @@ import static org.sonar.server.es.DefaultIndexSettings.NORMS; import static org.sonar.server.es.DefaultIndexSettings.STORE; import static org.sonar.server.es.DefaultIndexSettings.TYPE; import static org.sonar.server.es.DefaultIndexSettingsElement.UUID_MODULE_ANALYZER; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_ALLOW_ANYONE; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_GROUP_IDS; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_USER_IDS; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; public class NewIndex { @@ -181,10 +184,32 @@ public class NewIndex { } public NewIndexType requireProjectAuthorization() { - AuthorizationTypeSupport.enableProjectAuthorization(this); + enableProjectAuthorization(this); return this; } + /** + * Creates a type that requires to verify that user has the read permission + * when searching for documents. + * + * Both types {@code typeName} and "authorization" are created. Documents + * must be created with _parent and _routing having the parent uuid as values. + * + * @see NewIndex.NewIndexType#requireProjectAuthorization() + */ + private static NewIndex.NewIndexType enableProjectAuthorization(NewIndex.NewIndexType type) { + type.setAttribute("_parent", ImmutableMap.of("type", TYPE_AUTHORIZATION)); + type.setAttribute("_routing", ImmutableMap.of("required", true)); + + NewIndex.NewIndexType authType = type.getIndex().createType(TYPE_AUTHORIZATION); + authType.setAttribute("_routing", ImmutableMap.of("required", true)); + authType.createLongField(FIELD_GROUP_IDS); + authType.createLongField(FIELD_USER_IDS); + authType.createBooleanField(FIELD_ALLOW_ANYONE); + authType.setEnableSource(false); + return type; + } + public NewIndex getIndex() { return index; } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQuery.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQuery.java deleted file mode 100644 index 23e28bfd249..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQuery.java +++ /dev/null @@ -1,558 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue; - -import com.google.common.collect.ImmutableSet; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.Map; -import java.util.Set; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import org.apache.commons.lang.builder.ReflectionToStringBuilder; -import org.sonar.db.rule.RuleDefinitionDto; - -import static com.google.common.base.Preconditions.checkArgument; -import static org.sonar.server.es.SearchOptions.MAX_LIMIT; - -/** - * @since 3.6 - */ -public class IssueQuery { - - public static final String SORT_BY_CREATION_DATE = "CREATION_DATE"; - public static final String SORT_BY_UPDATE_DATE = "UPDATE_DATE"; - public static final String SORT_BY_CLOSE_DATE = "CLOSE_DATE"; - /** - * @deprecated since 7.2, it's no more possible to sort by assignee - */ - @Deprecated - public static final String SORT_BY_ASSIGNEE = "ASSIGNEE"; - public static final String SORT_BY_SEVERITY = "SEVERITY"; - public static final String SORT_BY_STATUS = "STATUS"; - - /** - * Sort by project, file path then line id - */ - public static final String SORT_BY_FILE_LINE = "FILE_LINE"; - - public static final Set SORTS = ImmutableSet.of(SORT_BY_CREATION_DATE, SORT_BY_UPDATE_DATE, SORT_BY_CLOSE_DATE, SORT_BY_ASSIGNEE, SORT_BY_SEVERITY, - SORT_BY_STATUS, SORT_BY_FILE_LINE); - - public static final String UNKNOWN_STANDARD = "unknown"; - public static final String SANS_TOP_25_INSECURE_INTERACTION = "insecure-interaction"; - public static final String SANS_TOP_25_RISKY_RESOURCE = "risky-resource"; - public static final String SANS_TOP_25_POROUS_DEFENSES = "porous-defenses"; - - private final Collection issueKeys; - private final Collection severities; - private final Collection statuses; - private final Collection resolutions; - private final Collection components; - private final Collection modules; - private final Collection moduleRoots; - private final Collection projects; - private final Collection directories; - private final Collection files; - private final Collection views; - private final Collection rules; - private final Collection assignees; - private final Collection authors; - private final Collection languages; - private final Collection tags; - private final Collection types; - private final Collection owaspTop10; - private final Collection sansTop25; - private final Collection cwe; - private final Map createdAfterByProjectUuids; - private final Boolean onComponentOnly; - private final Boolean assigned; - private final Boolean resolved; - private final Date createdAt; - private final PeriodStart createdAfter; - private final Date createdBefore; - private final String sort; - private final Boolean asc; - private final String facetMode; - private final String organizationUuid; - private final String branchUuid; - private final boolean mainBranch; - private final boolean checkAuthorization; - - private IssueQuery(Builder builder) { - this.issueKeys = defaultCollection(builder.issueKeys); - this.severities = defaultCollection(builder.severities); - this.statuses = defaultCollection(builder.statuses); - this.resolutions = defaultCollection(builder.resolutions); - this.components = defaultCollection(builder.components); - this.modules = defaultCollection(builder.modules); - this.moduleRoots = defaultCollection(builder.moduleRoots); - this.projects = defaultCollection(builder.projects); - this.directories = defaultCollection(builder.directories); - this.files = defaultCollection(builder.files); - this.views = defaultCollection(builder.views); - this.rules = defaultCollection(builder.rules); - this.assignees = defaultCollection(builder.assigneeUuids); - this.authors = defaultCollection(builder.authors); - this.languages = defaultCollection(builder.languages); - this.tags = defaultCollection(builder.tags); - this.types = defaultCollection(builder.types); - this.owaspTop10 = defaultCollection(builder.owaspTop10); - this.sansTop25 = defaultCollection(builder.sansTop25); - this.cwe = defaultCollection(builder.cwe); - this.createdAfterByProjectUuids = defaultMap(builder.createdAfterByProjectUuids); - this.onComponentOnly = builder.onComponentOnly; - this.assigned = builder.assigned; - this.resolved = builder.resolved; - this.createdAt = builder.createdAt; - this.createdAfter = builder.createdAfter; - this.createdBefore = builder.createdBefore; - this.sort = builder.sort; - this.asc = builder.asc; - this.checkAuthorization = builder.checkAuthorization; - this.facetMode = builder.facetMode; - this.organizationUuid = builder.organizationUuid; - this.branchUuid = builder.branchUuid; - this.mainBranch = builder.mainBranch; - } - - public Collection issueKeys() { - return issueKeys; - } - - public Collection severities() { - return severities; - } - - public Collection statuses() { - return statuses; - } - - public Collection resolutions() { - return resolutions; - } - - public Collection componentUuids() { - return components; - } - - public Collection moduleUuids() { - return modules; - } - - public Collection moduleRootUuids() { - return moduleRoots; - } - - public Collection projectUuids() { - return projects; - } - - public Collection directories() { - return directories; - } - - public Collection fileUuids() { - return files; - } - - public Collection viewUuids() { - return views; - } - - public Collection rules() { - return rules; - } - - public Collection assignees() { - return assignees; - } - - public Collection authors() { - return authors; - } - - public Collection languages() { - return languages; - } - - public Collection tags() { - return tags; - } - - public Collection types() { - return types; - } - - public Collection owaspTop10() { - return owaspTop10; - } - - public Collection sansTop25() { - return sansTop25; - } - - public Collection cwe() { - return cwe; - } - - public Map createdAfterByProjectUuids() { - return createdAfterByProjectUuids; - } - - @CheckForNull - public Boolean onComponentOnly() { - return onComponentOnly; - } - - @CheckForNull - public Boolean assigned() { - return assigned; - } - - @CheckForNull - public Boolean resolved() { - return resolved; - } - - @CheckForNull - public PeriodStart createdAfter() { - return createdAfter; - } - - @CheckForNull - public Date createdAt() { - return createdAt == null ? null : new Date(createdAt.getTime()); - } - - @CheckForNull - public Date createdBefore() { - return createdBefore == null ? null : new Date(createdBefore.getTime()); - } - - @CheckForNull - public String sort() { - return sort; - } - - @CheckForNull - public Boolean asc() { - return asc; - } - - public boolean checkAuthorization() { - return checkAuthorization; - } - - @CheckForNull - public String organizationUuid() { - return organizationUuid; - } - - @CheckForNull - public String branchUuid() { - return branchUuid; - } - - public boolean isMainBranch() { - return mainBranch; - } - - public String facetMode() { - return facetMode; - } - - @Override - public String toString() { - return ReflectionToStringBuilder.toString(this); - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - private Collection issueKeys; - private Collection severities; - private Collection statuses; - private Collection resolutions; - private Collection components; - private Collection modules; - private Collection moduleRoots; - private Collection projects; - private Collection directories; - private Collection files; - private Collection views; - private Collection rules; - private Collection assigneeUuids; - private Collection authors; - private Collection languages; - private Collection tags; - private Collection types; - private Collection owaspTop10; - private Collection sansTop25; - private Collection cwe; - private Map createdAfterByProjectUuids; - private Boolean onComponentOnly = false; - private Boolean assigned = null; - private Boolean resolved = null; - private Date createdAt; - private PeriodStart createdAfter; - private Date createdBefore; - private String sort; - private Boolean asc = false; - private boolean checkAuthorization = true; - private String facetMode; - private String organizationUuid; - private String branchUuid; - private boolean mainBranch = true; - - private Builder() { - - } - - public Builder issueKeys(@Nullable Collection l) { - this.issueKeys = l; - return this; - } - - public Builder severities(@Nullable Collection l) { - this.severities = l; - return this; - } - - public Builder statuses(@Nullable Collection l) { - this.statuses = l; - return this; - } - - public Builder resolutions(@Nullable Collection l) { - this.resolutions = l; - return this; - } - - public Builder componentUuids(@Nullable Collection l) { - this.components = l; - return this; - } - - public Builder moduleUuids(@Nullable Collection l) { - this.modules = l; - return this; - } - - public Builder moduleRootUuids(@Nullable Collection l) { - this.moduleRoots = l; - return this; - } - - public Builder projectUuids(@Nullable Collection l) { - this.projects = l; - return this; - } - - public Builder directories(@Nullable Collection l) { - this.directories = l; - return this; - } - - public Builder fileUuids(@Nullable Collection l) { - this.files = l; - return this; - } - - public Builder viewUuids(@Nullable Collection l) { - this.views = l; - return this; - } - - public Builder rules(@Nullable Collection rules) { - this.rules = rules; - return this; - } - - public Builder assigneeUuids(@Nullable Collection l) { - this.assigneeUuids = l; - return this; - } - - public Builder authors(@Nullable Collection l) { - this.authors = l; - return this; - } - - public Builder languages(@Nullable Collection l) { - this.languages = l; - return this; - } - - public Builder tags(@Nullable Collection t) { - this.tags = t; - return this; - } - - public Builder types(@Nullable Collection t) { - this.types = t; - return this; - } - - public Builder owaspTop10(@Nullable Collection o) { - this.owaspTop10 = o; - return this; - } - - public Builder sansTop25(@Nullable Collection s) { - this.sansTop25 = s; - return this; - } - - public Builder cwe(@Nullable Collection cwe) { - this.cwe = cwe; - return this; - } - - public Builder createdAfterByProjectUuids(@Nullable Map createdAfterByProjectUuids) { - this.createdAfterByProjectUuids = createdAfterByProjectUuids; - return this; - } - - /** - * If true, it will return only issues on the passed component(s) - * If false, it will return all issues on the passed component(s) and their descendants - */ - public Builder onComponentOnly(@Nullable Boolean b) { - this.onComponentOnly = b; - return this; - } - - /** - * If true, it will return all issues assigned to someone - * If false, it will return all issues not assigned to someone - */ - public Builder assigned(@Nullable Boolean b) { - this.assigned = b; - return this; - } - - /** - * If true, it will return all resolved issues - * If false, it will return all none resolved issues - */ - public Builder resolved(@Nullable Boolean resolved) { - this.resolved = resolved; - return this; - } - - public Builder createdAt(@Nullable Date d) { - this.createdAt = d == null ? null : new Date(d.getTime()); - return this; - } - - public Builder createdAfter(@Nullable Date d) { - this.createdAfter(d, true); - return this; - } - - public Builder createdAfter(@Nullable Date d, boolean inclusive) { - this.createdAfter = d == null ? null : new PeriodStart(new Date(d.getTime()), inclusive); - return this; - } - - public Builder createdBefore(@Nullable Date d) { - this.createdBefore = d == null ? null : new Date(d.getTime()); - return this; - } - - public Builder sort(@Nullable String s) { - if (s != null && !SORTS.contains(s)) { - throw new IllegalArgumentException("Bad sort field: " + s); - } - this.sort = s; - return this; - } - - public Builder asc(@Nullable Boolean asc) { - this.asc = asc; - return this; - } - - public IssueQuery build() { - if (issueKeys != null) { - checkArgument(issueKeys.size() <= MAX_LIMIT, "Number of issue keys must be less than " + MAX_LIMIT + " (got " + issueKeys.size() + ")"); - } - return new IssueQuery(this); - } - - public Builder checkAuthorization(boolean checkAuthorization) { - this.checkAuthorization = checkAuthorization; - return this; - } - - public Builder facetMode(String facetMode) { - this.facetMode = facetMode; - return this; - } - - public Builder organizationUuid(String s) { - this.organizationUuid = s; - return this; - } - - public Builder branchUuid(@Nullable String s) { - this.branchUuid = s; - return this; - } - - public Builder mainBranch(boolean mainBranch) { - this.mainBranch = mainBranch; - return this; - } - } - - private static Collection defaultCollection(@Nullable Collection c) { - return c == null ? Collections.emptyList() : Collections.unmodifiableCollection(c); - } - - private static Map defaultMap(@Nullable Map map) { - return map == null ? Collections.emptyMap() : Collections.unmodifiableMap(map); - } - - public static class PeriodStart { - private final Date date; - private final boolean inclusive; - - public PeriodStart(Date date, boolean inclusive) { - this.date = date; - this.inclusive = inclusive; - - } - - public Date date() { - return date; - } - - public boolean inclusive() { - return inclusive; - } - - } - -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQueryFactory.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQueryFactory.java deleted file mode 100644 index e28607edc5b..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueQueryFactory.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue; - -import com.google.common.base.Joiner; -import com.google.common.base.Strings; -import java.time.Clock; -import java.time.OffsetDateTime; -import java.time.Period; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import org.apache.commons.lang.BooleanUtils; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.rule.RuleKey; -import org.sonar.api.server.ServerSide; -import org.sonar.api.web.UserRole; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.SnapshotDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.server.issue.IssueQuery.PeriodStart; -import org.sonar.server.user.UserSession; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Collections2.transform; -import static java.lang.String.format; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.sonar.api.utils.DateUtils.longToDate; -import static org.sonar.api.utils.DateUtils.parseDateOrDateTime; -import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime; -import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime; -import static org.sonar.core.util.stream.MoreCollectors.toHashSet; -import static org.sonar.core.util.stream.MoreCollectors.toList; -import static org.sonar.core.util.stream.MoreCollectors.toSet; -import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_ROOTS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_LAST; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD; - -/** - * This component is used to create an IssueQuery, in order to transform the component and component roots keys into uuid. - */ -@ServerSide -public class IssueQueryFactory { - - public static final String UNKNOWN = ""; - private static final ComponentDto UNKNOWN_COMPONENT = new ComponentDto().setUuid(UNKNOWN).setProjectUuid(UNKNOWN); - - private final DbClient dbClient; - private final Clock clock; - private final UserSession userSession; - - public IssueQueryFactory(DbClient dbClient, Clock clock, UserSession userSession) { - this.dbClient = dbClient; - this.clock = clock; - this.userSession = userSession; - } - - public IssueQuery create(SearchRequest request) { - try (DbSession dbSession = dbClient.openSession(false)) { - IssueQuery.Builder builder = IssueQuery.builder() - .issueKeys(request.getIssues()) - .severities(request.getSeverities()) - .statuses(request.getStatuses()) - .resolutions(request.getResolutions()) - .resolved(request.getResolved()) - .rules(ruleKeysToRuleId(dbSession, request.getRules())) - .assigneeUuids(request.getAssigneeUuids()) - .languages(request.getLanguages()) - .tags(request.getTags()) - .types(request.getTypes()) - .owaspTop10(request.getOwaspTop10()) - .sansTop25(request.getSansTop25()) - .cwe(request.getCwe()) - .assigned(request.getAssigned()) - .createdAt(parseDateOrDateTime(request.getCreatedAt())) - .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore())) - .facetMode(request.getFacetMode()) - .organizationUuid(convertOrganizationKeyToUuid(dbSession, request.getOrganization())); - - List allComponents = new ArrayList<>(); - boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents); - addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request); - - setCreatedAfterFromRequest(dbSession, builder, request, allComponents); - String sort = request.getSort(); - if (!Strings.isNullOrEmpty(sort)) { - builder.sort(sort); - builder.asc(request.getAsc()); - } - return builder.build(); - } - } - - private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast, boolean createdAfterInclusive) { - checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST)); - - Date actualCreatedAfter = createdAfter; - if (createdInLast != null) { - actualCreatedAfter = Date.from( - OffsetDateTime.now(clock) - .minus(Period.parse("P" + createdInLast.toUpperCase(Locale.ENGLISH))) - .toInstant()); - } - builder.createdAfter(actualCreatedAfter, createdAfterInclusive); - } - - @CheckForNull - private String convertOrganizationKeyToUuid(DbSession dbSession, @Nullable String organizationKey) { - if (organizationKey == null) { - return null; - } - Optional organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey); - return organization.map(OrganizationDto::getUuid).orElse(UNKNOWN); - } - - private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List componentUuids) { - Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter()); - String createdInLast = request.getCreatedInLast(); - - if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) { - setCreatedAfterFromDates(builder, createdAfter, createdInLast, true); - } else { - checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_SINCE_LEAK_PERIOD); - checkArgument(componentUuids.size() == 1, "One and only one component must be provided when searching since leak period"); - ComponentDto component = componentUuids.iterator().next(); - Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(dbSession, component); - setCreatedAfterFromDates(builder, createdAfterFromSnapshot, createdInLast, false); - } - } - - @CheckForNull - private Date findCreatedAfterFromComponentUuid(DbSession dbSession, ComponentDto component) { - Optional snapshot = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); - return snapshot.map(s -> longToDate(s.getPeriodDate())).orElse(null); - } - - private boolean mergeDeprecatedComponentParameters(DbSession session, SearchRequest request, List allComponents) { - Boolean onComponentOnly = request.getOnComponentOnly(); - Collection components = request.getComponents(); - Collection componentUuids = request.getComponentUuids(); - Collection componentKeys = request.getComponentKeys(); - Collection componentRootUuids = request.getComponentRootUuids(); - Collection componentRoots = request.getComponentRoots(); - String branch = request.getBranch(); - String pullRequest = request.getPullRequest(); - - boolean effectiveOnComponentOnly = false; - - checkArgument(atMostOneNonNullElement(components, componentUuids, componentKeys, componentRootUuids, componentRoots), - "At most one of the following parameters can be provided: %s, %s, %s, %s, %s", - PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS, PARAM_COMPONENTS, PARAM_COMPONENT_ROOTS, PARAM_COMPONENT_UUIDS); - - if (componentRootUuids != null) { - allComponents.addAll(getComponentsFromUuids(session, componentRootUuids)); - } else if (componentRoots != null) { - allComponents.addAll(getComponentsFromKeys(session, componentRoots, branch, pullRequest)); - } else if (components != null) { - allComponents.addAll(getComponentsFromKeys(session, components, branch, pullRequest)); - effectiveOnComponentOnly = true; - } else if (componentUuids != null) { - allComponents.addAll(getComponentsFromUuids(session, componentUuids)); - effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly); - } else if (componentKeys != null) { - allComponents.addAll(getComponentsFromKeys(session, componentKeys, branch, pullRequest)); - effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly); - } - - return effectiveOnComponentOnly; - } - - private static boolean atMostOneNonNullElement(Object... objects) { - return Arrays.stream(objects) - .filter(Objects::nonNull) - .count() <= 1; - } - - private void addComponentParameters(IssueQuery.Builder builder, DbSession session, boolean onComponentOnly, List components, SearchRequest request) { - builder.onComponentOnly(onComponentOnly); - if (onComponentOnly) { - builder.componentUuids(components.stream().map(ComponentDto::uuid).collect(toList())); - setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest()); - return; - } - - builder.authors(request.getAuthors()); - List projectUuids = request.getProjectUuids(); - List projectKeys = request.getProjectKeys(); - checkArgument(projectUuids == null || projectKeys == null, "Parameters projects and projectUuids cannot be set simultaneously"); - if (projectUuids != null) { - builder.projectUuids(projectUuids); - } else if (projectKeys != null) { - List projects = getComponentsFromKeys(session, projectKeys, request.getBranch(), request.getPullRequest()); - builder.projectUuids(projects.stream().map(IssueQueryFactory::toProjectUuid).collect(toList())); - setBranch(builder, projects.get(0), request.getBranch(), request.getPullRequest()); - } - builder.moduleUuids(request.getModuleUuids()); - builder.directories(request.getDirectories()); - builder.fileUuids(request.getFileUuids()); - - addComponentsBasedOnQualifier(builder, session, components, request); - } - - private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List components, SearchRequest request) { - if (components.isEmpty()) { - return; - } - if (components.stream().map(ComponentDto::uuid).anyMatch(uuid -> uuid.equals(UNKNOWN))) { - builder.componentUuids(singleton(UNKNOWN)); - return; - } - - Set qualifiers = components.stream().map(ComponentDto::qualifier).collect(toHashSet()); - checkArgument(qualifiers.size() == 1, "All components must have the same qualifier, found %s", String.join(",", qualifiers)); - - setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest()); - String qualifier = qualifiers.iterator().next(); - switch (qualifier) { - case Qualifiers.VIEW: - case Qualifiers.SUBVIEW: - addViewsOrSubViews(builder, components); - break; - case Qualifiers.APP: - addApplications(builder, dbSession, components, request); - break; - case Qualifiers.PROJECT: - builder.projectUuids(components.stream().map(IssueQueryFactory::toProjectUuid).collect(toList())); - break; - case Qualifiers.MODULE: - builder.moduleRootUuids(components.stream().map(ComponentDto::uuid).collect(toList())); - break; - case Qualifiers.DIRECTORY: - addDirectories(builder, components); - break; - case Qualifiers.FILE: - case Qualifiers.UNIT_TEST_FILE: - builder.fileUuids(components.stream().map(ComponentDto::uuid).collect(toList())); - break; - default: - throw new IllegalArgumentException("Unable to set search root context for components " + Joiner.on(',').join(components)); - } - } - - private void addViewsOrSubViews(IssueQuery.Builder builder, Collection viewOrSubViewUuids) { - List filteredViewUuids = viewOrSubViewUuids.stream() - .filter(uuid -> userSession.hasComponentPermission(UserRole.USER, uuid)) - .map(ComponentDto::uuid) - .collect(Collectors.toList()); - if (filteredViewUuids.isEmpty()) { - filteredViewUuids.add(UNKNOWN); - } - builder.viewUuids(filteredViewUuids); - } - - private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List applications, SearchRequest request) { - Set authorizedApplicationUuids = applications.stream() - .filter(app -> userSession.hasComponentPermission(UserRole.USER, app)) - .map(ComponentDto::uuid) - .collect(toSet()); - - builder.viewUuids(authorizedApplicationUuids.isEmpty() ? singleton(UNKNOWN) : authorizedApplicationUuids); - addCreatedAfterByProjects(builder, dbSession, request, authorizedApplicationUuids); - } - - private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request, Set applicationUuids) { - if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) { - return; - } - - Set projectUuids = applicationUuids.stream() - .flatMap(app -> dbClient.componentDao().selectProjectsFromView(dbSession, app, app).stream()) - .collect(toSet()); - - Map leakByProjects = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids) - .stream() - .filter(s -> s.getPeriodDate() != null) - .collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> new PeriodStart(longToDate(s.getPeriodDate()), false))); - builder.createdAfterByProjectUuids(leakByProjects); - } - - private static void addDirectories(IssueQuery.Builder builder, List directories) { - Collection directoryModuleUuids = new HashSet<>(); - Collection directoryPaths = new HashSet<>(); - for (ComponentDto directory : directories) { - directoryModuleUuids.add(directory.moduleUuid()); - directoryPaths.add(directory.path()); - } - builder.moduleUuids(directoryModuleUuids); - builder.directories(directoryPaths); - } - - private List getComponentsFromKeys(DbSession dbSession, Collection componentKeys, @Nullable String branch, @Nullable String pullRequest) { - List componentDtos; - if (branch != null) { - componentDtos = dbClient.componentDao().selectByKeysAndBranch(dbSession, componentKeys, branch); - } else if (pullRequest != null) { - componentDtos = dbClient.componentDao().selectByKeysAndPullRequest(dbSession, componentKeys, pullRequest); - } else { - componentDtos = dbClient.componentDao().selectByKeys(dbSession, componentKeys); - } - if (!componentKeys.isEmpty() && componentDtos.isEmpty()) { - return singletonList(UNKNOWN_COMPONENT); - } - return componentDtos; - } - - private List getComponentsFromUuids(DbSession dbSession, Collection componentUuids) { - List componentDtos = dbClient.componentDao().selectByUuids(dbSession, componentUuids); - if (!componentUuids.isEmpty() && componentDtos.isEmpty()) { - return singletonList(UNKNOWN_COMPONENT); - } - return componentDtos; - } - - @CheckForNull - private Collection ruleKeysToRuleId(DbSession dbSession, @Nullable Collection rules) { - if (rules != null) { - return dbClient.ruleDao().selectDefinitionByKeys(dbSession, transform(rules, RuleKey::parse)); - } - return Collections.emptyList(); - } - - private static String toProjectUuid(ComponentDto componentDto) { - String mainBranchProjectUuid = componentDto.getMainBranchProjectUuid(); - return mainBranchProjectUuid == null ? componentDto.projectUuid() : mainBranchProjectUuid; - } - - private static void setBranch(IssueQuery.Builder builder, ComponentDto component, @Nullable String branch, @Nullable String pullRequest) { - builder.branchUuid(branch == null && pullRequest == null ? null : component.projectUuid()); - builder.mainBranch(UNKNOWN_COMPONENT.equals(component) - || (branch == null && pullRequest == null) - || (branch != null && !branch.equals(component.getBranch())) - || (pullRequest != null && !pullRequest.equals(component.getPullRequest()))); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndex.java deleted file mode 100644 index 5c0522d9e54..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndex.java +++ /dev/null @@ -1,919 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue.index; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Maps; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.OptionalInt; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import org.apache.commons.lang.BooleanUtils; -import org.apache.commons.lang.StringUtils; -import org.elasticsearch.action.search.SearchRequestBuilder; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.indices.TermsLookup; -import org.elasticsearch.search.aggregations.AggregationBuilder; -import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.HasAggregations; -import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; -import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; -import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds; -import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms; -import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; -import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; -import org.elasticsearch.search.aggregations.metrics.max.InternalMax; -import org.elasticsearch.search.aggregations.metrics.min.Min; -import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder; -import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; -import org.elasticsearch.search.sort.FieldSortBuilder; -import org.joda.time.DateTimeZone; -import org.joda.time.Duration; -import org.sonar.api.issue.Issue; -import org.sonar.api.rule.Severity; -import org.sonar.api.rules.RuleType; -import org.sonar.api.utils.DateUtils; -import org.sonar.api.utils.System2; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.server.es.BaseDoc; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.EsUtils; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.es.Sorting; -import org.sonar.server.es.StickyFacetBuilder; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQuery.PeriodStart; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.user.UserSession; -import org.sonar.server.view.index.ViewIndexDefinition; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkState; -import static java.lang.String.format; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.existsQuery; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.elasticsearch.index.query.QueryBuilders.termsQuery; -import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; -import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds; -import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; -import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID; -import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_ACTION_PLANS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_ASSIGNED_TO_ME; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHORS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILE_UUIDS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_UUIDS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_REPORTERS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITIES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPES; - -/** - * The unique entry-point to interact with Elasticsearch index "issues". - * All the requests are listed here. - */ -public class IssueIndex { - - public static final List SUPPORTED_FACETS = ImmutableList.of( - PARAM_SEVERITIES, - PARAM_STATUSES, - PARAM_RESOLUTIONS, - DEPRECATED_PARAM_ACTION_PLANS, - PARAM_PROJECT_UUIDS, - PARAM_RULES, - PARAM_ASSIGNEES, - FACET_ASSIGNED_TO_ME, - PARAM_REPORTERS, - PARAM_AUTHORS, - PARAM_MODULE_UUIDS, - PARAM_FILE_UUIDS, - PARAM_DIRECTORIES, - PARAM_LANGUAGES, - PARAM_TAGS, - PARAM_TYPES, - PARAM_OWASP_TOP_10, - PARAM_SANS_TOP_25, - PARAM_CWE, - PARAM_CREATED_AT); - public static final String AGGREGATION_NAME_FOR_TAGS = "tags__issues"; - private static final String SUBSTRING_MATCH_REGEXP = ".*%s.*"; - // TODO to be documented - // TODO move to Facets ? - private static final String FACET_SUFFIX_MISSING = "_missing"; - private static final String IS_ASSIGNED_FILTER = "__isAssigned"; - private static final SumAggregationBuilder EFFORT_AGGREGATION = AggregationBuilders.sum(FACET_MODE_EFFORT).field(IssueIndexDefinition.FIELD_ISSUE_EFFORT); - private static final Order EFFORT_AGGREGATION_ORDER = Order.aggregation(FACET_MODE_EFFORT, false); - private static final int DEFAULT_FACET_SIZE = 15; - private static final Duration TWENTY_DAYS = Duration.standardDays(20L); - private static final Duration TWENTY_WEEKS = Duration.standardDays(20L * 7L); - private static final Duration TWENTY_MONTHS = Duration.standardDays(20L * 30L); - private static final String COUNT = "count"; - private final Sorting sorting; - private final EsClient client; - private final System2 system; - private final UserSession userSession; - private final AuthorizationTypeSupport authorizationTypeSupport; - - public IssueIndex(EsClient client, System2 system, UserSession userSession, AuthorizationTypeSupport authorizationTypeSupport) { - this.client = client; - this.system = system; - this.userSession = userSession; - this.authorizationTypeSupport = authorizationTypeSupport; - - this.sorting = new Sorting(); - this.sorting.add(IssueQuery.SORT_BY_STATUS, IssueIndexDefinition.FIELD_ISSUE_STATUS); - this.sorting.add(IssueQuery.SORT_BY_SEVERITY, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE); - this.sorting.add(IssueQuery.SORT_BY_CREATION_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT); - this.sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT); - this.sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT); - this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID); - this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_FILE_PATH); - this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_LINE); - this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE).reverse(); - this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_KEY); - - // by default order by created date, project, file, line and issue key (in order to be deterministic when same ms) - this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).reverse(); - this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID); - this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FILE_PATH); - this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_LINE); - this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_KEY); - } - - public SearchResponse search(IssueQuery query, SearchOptions options) { - SearchRequestBuilder requestBuilder = client.prepareSearch(INDEX_TYPE_ISSUE); - - configureSorting(query, requestBuilder); - configurePagination(options, requestBuilder); - configureRouting(query, options, requestBuilder); - - QueryBuilder esQuery = matchAllQuery(); - BoolQueryBuilder esFilter = boolQuery(); - Map filters = createFilters(query); - for (QueryBuilder filter : filters.values()) { - if (filter != null) { - esFilter.must(filter); - } - } - if (esFilter.hasClauses()) { - requestBuilder.setQuery(boolQuery().must(esQuery).filter(esFilter)); - } else { - requestBuilder.setQuery(esQuery); - } - - configureStickyFacets(query, options, filters, esQuery, requestBuilder); - requestBuilder.setFetchSource(false); - return requestBuilder.get(); - } - - /** - * Optimization - do not send ES request to all shards when scope is restricted - * to a set of projects. Because project UUID is used for routing, the request - * can be sent to only the shards containing the specified projects. - * Note that sticky facets may involve all projects, so this optimization must be - * disabled when facets are enabled. - */ - private static void configureRouting(IssueQuery query, SearchOptions options, SearchRequestBuilder requestBuilder) { - Collection uuids = query.projectUuids(); - if (!uuids.isEmpty() && options.getFacets().isEmpty()) { - requestBuilder.setRouting(uuids.toArray(new String[uuids.size()])); - } - } - - private static void configurePagination(SearchOptions options, SearchRequestBuilder esSearch) { - esSearch.setFrom(options.getOffset()).setSize(options.getLimit()); - } - - private Map createFilters(IssueQuery query) { - Map filters = new HashMap<>(); - filters.put("__authorization", createAuthorizationFilter(query.checkAuthorization())); - - // Issue is assigned Filter - if (BooleanUtils.isTrue(query.assigned())) { - filters.put(IS_ASSIGNED_FILTER, existsQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID)); - } else if (BooleanUtils.isFalse(query.assigned())) { - filters.put(IS_ASSIGNED_FILTER, boolQuery().mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID))); - } - - // Issue is Resolved Filter - String isResolved = "__isResolved"; - if (BooleanUtils.isTrue(query.resolved())) { - filters.put(isResolved, existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)); - } else if (BooleanUtils.isFalse(query.resolved())) { - filters.put(isResolved, boolQuery().mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))); - } - - // Field Filters - filters.put(IssueIndexDefinition.FIELD_ISSUE_KEY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_KEY, query.issueKeys())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, query.assignees())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_TAGS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_TYPE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_TYPE, query.types())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, query.resolutions())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_RULE_ID, createTermsFilter( - IssueIndexDefinition.FIELD_ISSUE_RULE_ID, - query.rules().stream().map(RuleDefinitionDto::getId).collect(Collectors.toList()))); - filters.put(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, query.severities())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_STATUS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_STATUS, query.statuses())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, createTermFilter(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, query.organizationUuid())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, query.owaspTop10())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, query.sansTop25())); - filters.put(IssueIndexDefinition.FIELD_ISSUE_CWE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_CWE, query.cwe())); - - addComponentRelatedFilters(query, filters); - - addDatesFilter(filters, query); - addCreatedAfterByProjectsFilter(filters, query); - return filters; - } - - private static void addComponentRelatedFilters(IssueQuery query, Map filters) { - addCommonComponentRelatedFilters(query, filters); - if (query.viewUuids().isEmpty()) { - addBranchComponentRelatedFilters(query, filters); - } else { - addViewRelatedFilters(query, filters); - } - } - - private static void addCommonComponentRelatedFilters(IssueQuery query, Map filters) { - QueryBuilder componentFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.componentUuids()); - QueryBuilder projectFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids()); - QueryBuilder moduleRootFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH, query.moduleRootUuids()); - QueryBuilder moduleFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids()); - QueryBuilder directoryFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories()); - QueryBuilder fileFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids()); - - if (BooleanUtils.isTrue(query.onComponentOnly())) { - filters.put(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, componentFilter); - } else { - filters.put(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectFilter); - filters.put("__module", moduleRootFilter); - filters.put(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, moduleFilter); - filters.put(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, directoryFilter); - filters.put(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, fileFilter != null ? fileFilter : componentFilter); - } - } - - private static void addBranchComponentRelatedFilters(IssueQuery query, Map filters) { - if (BooleanUtils.isTrue(query.onComponentOnly())) { - return; - } - QueryBuilder branchFilter = createTermFilter(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, query.branchUuid()); - filters.put("__is_main_branch", createTermFilter(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(query.isMainBranch()))); - filters.put(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, branchFilter); - } - - private static void addViewRelatedFilters(IssueQuery query, Map filters) { - if (BooleanUtils.isTrue(query.onComponentOnly())) { - return; - } - Collection viewUuids = query.viewUuids(); - String branchUuid = query.branchUuid(); - boolean onApplicationBranch = branchUuid != null && !viewUuids.isEmpty(); - if (onApplicationBranch) { - filters.put("__view", createViewFilter(singletonList(query.branchUuid()))); - } else { - filters.put("__is_main_branch", createTermFilter(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(true))); - filters.put("__view", createViewFilter(viewUuids)); - } - } - - @CheckForNull - private static QueryBuilder createViewFilter(Collection viewUuids) { - if (viewUuids.isEmpty()) { - return null; - } - - BoolQueryBuilder viewsFilter = boolQuery(); - for (String viewUuid : viewUuids) { - viewsFilter.should(QueryBuilders.termsLookupQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, - new TermsLookup( - ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), - ViewIndexDefinition.INDEX_TYPE_VIEW.getType(), - viewUuid, - ViewIndexDefinition.FIELD_PROJECTS))); - } - return viewsFilter; - } - - private static StickyFacetBuilder newStickyFacetBuilder(IssueQuery query, Map filters, QueryBuilder esQuery) { - if (hasQueryEffortFacet(query)) { - return new StickyFacetBuilder(esQuery, filters, EFFORT_AGGREGATION, EFFORT_AGGREGATION_ORDER); - } - return new StickyFacetBuilder(esQuery, filters); - } - - private static void addSimpleStickyFacetIfNeeded(SearchOptions options, StickyFacetBuilder stickyFacetBuilder, SearchRequestBuilder esSearch, - String facetName, String fieldName, Object... selectedValues) { - if (options.getFacets().contains(facetName)) { - esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldName, facetName, DEFAULT_FACET_SIZE, selectedValues)); - } - } - - private static AggregationBuilder addEffortAggregationIfNeeded(IssueQuery query, AggregationBuilder aggregation) { - if (hasQueryEffortFacet(query)) { - aggregation.subAggregation(EFFORT_AGGREGATION); - } - return aggregation; - } - - private static boolean hasQueryEffortFacet(IssueQuery query) { - return FACET_MODE_EFFORT.equals(query.facetMode()) || DEPRECATED_FACET_MODE_DEBT.equals(query.facetMode()); - } - - private static AggregationBuilder createAssigneesFacet(IssueQuery query, Map filters, QueryBuilder queryBuilder) { - String fieldName = IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID; - String facetName = PARAM_ASSIGNEES; - - // Same as in super.stickyFacetBuilder - Map assigneeFilters = Maps.newHashMap(filters); - assigneeFilters.remove(IS_ASSIGNED_FILTER); - assigneeFilters.remove(fieldName); - StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, assigneeFilters, queryBuilder); - BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); - FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE); - - Collection assigneesEscaped = escapeValuesForFacetInclusion(query.assignees()); - if (!assigneesEscaped.isEmpty()) { - facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t, assigneesEscaped.toArray()); - } - - // Add missing facet for unassigned issues - facetTopAggregation.subAggregation( - addEffortAggregationIfNeeded(query, AggregationBuilders - .missing(facetName + FACET_SUFFIX_MISSING) - .field(fieldName))); - - return AggregationBuilders - .global(facetName) - .subAggregation(facetTopAggregation); - } - - private static Collection escapeValuesForFacetInclusion(@Nullable Collection values) { - if (values == null) { - return Collections.emptyList(); - } - return values.stream().map(Pattern::quote).collect(MoreCollectors.toArrayList(values.size())); - } - - private static AggregationBuilder createResolutionFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { - String fieldName = IssueIndexDefinition.FIELD_ISSUE_RESOLUTION; - String facetName = PARAM_RESOLUTIONS; - - // Same as in super.stickyFacetBuilder - Map resolutionFilters = Maps.newHashMap(filters); - resolutionFilters.remove("__isResolved"); - resolutionFilters.remove(fieldName); - StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, resolutionFilters, esQuery); - BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); - FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE); - facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t); - - // Add missing facet for unresolved issues - facetTopAggregation.subAggregation( - addEffortAggregationIfNeeded(query, AggregationBuilders - .missing(facetName + FACET_SUFFIX_MISSING) - .field(fieldName))); - - return AggregationBuilders - .global(facetName) - .subAggregation(facetTopAggregation); - } - - @CheckForNull - private static QueryBuilder createTermsFilter(String field, Collection values) { - return values.isEmpty() ? null : termsQuery(field, values); - } - - @CheckForNull - private static QueryBuilder createTermFilter(String field, @Nullable String value) { - return value == null ? null : termQuery(field, value); - } - - private void configureSorting(IssueQuery query, SearchRequestBuilder esRequest) { - createSortBuilders(query).forEach(esRequest::addSort); - } - - private List createSortBuilders(IssueQuery query) { - String sortField = query.sort(); - if (sortField != null) { - boolean asc = BooleanUtils.isTrue(query.asc()); - return sorting.fill(sortField, asc); - } - return sorting.fillDefault(); - } - - private QueryBuilder createAuthorizationFilter(boolean checkAuthorization) { - if (checkAuthorization) { - return authorizationTypeSupport.createQueryFilter(); - } - return matchAllQuery(); - } - - private void addDatesFilter(Map filters, IssueQuery query) { - PeriodStart createdAfter = query.createdAfter(); - Date createdBefore = query.createdBefore(); - - validateCreationDateBounds(createdBefore, createdAfter != null ? createdAfter.date() : null); - - if (createdAfter != null) { - filters.put("__createdAfter", QueryBuilders - .rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) - .from(BaseDoc.dateToEpochSeconds(createdAfter.date()), createdAfter.inclusive())); - } - if (createdBefore != null) { - filters.put("__createdBefore", QueryBuilders - .rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) - .lt(BaseDoc.dateToEpochSeconds(createdBefore))); - } - Date createdAt = query.createdAt(); - if (createdAt != null) { - filters.put("__createdAt", termQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT, BaseDoc.dateToEpochSeconds(createdAt))); - } - } - - private static void addCreatedAfterByProjectsFilter(Map filters, IssueQuery query) { - Map createdAfterByProjectUuids = query.createdAfterByProjectUuids(); - BoolQueryBuilder boolQueryBuilder = boolQuery(); - createdAfterByProjectUuids.forEach((projectUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid)) - .filter(rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).from(BaseDoc.dateToEpochSeconds(createdAfterDate.date()), createdAfterDate.inclusive())))); - filters.put("createdAfterByProjectUuids", boolQueryBuilder); - } - - private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) { - Preconditions.checkArgument(createdAfter == null || createdAfter.before(new Date(system.now())), - "Start bound cannot be in the future"); - Preconditions.checkArgument(createdAfter == null || createdBefore == null || createdAfter.before(createdBefore), - "Start bound cannot be larger or equal to end bound"); - } - - private void configureStickyFacets(IssueQuery query, SearchOptions options, Map filters, QueryBuilder esQuery, SearchRequestBuilder esSearch) { - if (!options.getFacets().isEmpty()) { - StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, filters, esQuery); - // Execute Term aggregations - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_SEVERITIES, IssueIndexDefinition.FIELD_ISSUE_SEVERITY); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_STATUSES, IssueIndexDefinition.FIELD_ISSUE_STATUS); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_PROJECT_UUIDS, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids().toArray()); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_MODULE_UUIDS, IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids().toArray()); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_DIRECTORIES, IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories().toArray()); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_FILE_UUIDS, IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids().toArray()); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_LANGUAGES, IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages().toArray()); - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_RULES, IssueIndexDefinition.FIELD_ISSUE_RULE_ID, query.rules().stream().map(RuleDefinitionDto::getId).toArray()); - - addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, - PARAM_AUTHORS, IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors().toArray()); - - addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_TAGS, IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags()); - addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_TYPES, IssueIndexDefinition.FIELD_ISSUE_TYPE, query.types()); - addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_OWASP_TOP_10, IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, query.owaspTop10()); - addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_SANS_TOP_25, IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, query.sansTop25()); - addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_CWE, IssueIndexDefinition.FIELD_ISSUE_CWE, query.cwe()); - if (options.getFacets().contains(PARAM_RESOLUTIONS)) { - esSearch.addAggregation(createResolutionFacet(query, filters, esQuery)); - } - if (options.getFacets().contains(PARAM_ASSIGNEES)) { - esSearch.addAggregation(createAssigneesFacet(query, filters, esQuery)); - } - addAssignedToMeFacetIfNeeded(esSearch, options, query, filters, esQuery); - if (options.getFacets().contains(PARAM_CREATED_AT)) { - getCreatedAtFacet(query, filters, esQuery).ifPresent(esSearch::addAggregation); - } - } - - if (hasQueryEffortFacet(query)) { - esSearch.addAggregation(EFFORT_AGGREGATION); - } - } - - private static void addStickyFacetIfNeeded(SearchOptions options, SearchRequestBuilder esSearch, StickyFacetBuilder stickyFacetBuilder, String paramTags, String fieldIssueTags, - Collection tags) { - if (options.getFacets().contains(paramTags)) { - esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldIssueTags, paramTags, tags.toArray())); - } - } - - private Optional getCreatedAtFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { - long startTime; - boolean startInclusive; - PeriodStart createdAfter = query.createdAfter(); - if (createdAfter == null) { - Optional minDate = getMinCreatedAt(filters, esQuery); - if (!minDate.isPresent()) { - return Optional.empty(); - } - startTime = minDate.get(); - startInclusive = true; - } else { - startTime = createdAfter.date().getTime(); - startInclusive = createdAfter.inclusive(); - } - Date createdBefore = query.createdBefore(); - long endTime = createdBefore == null ? system.now() : createdBefore.getTime(); - - Duration timeSpan = new Duration(startTime, endTime); - DateHistogramInterval bucketSize = DateHistogramInterval.YEAR; - if (timeSpan.isShorterThan(TWENTY_DAYS)) { - bucketSize = DateHistogramInterval.DAY; - } else if (timeSpan.isShorterThan(TWENTY_WEEKS)) { - bucketSize = DateHistogramInterval.WEEK; - } else if (timeSpan.isShorterThan(TWENTY_MONTHS)) { - bucketSize = DateHistogramInterval.MONTH; - } - - AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(PARAM_CREATED_AT) - .field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) - .dateHistogramInterval(bucketSize) - .minDocCount(0L) - .format(DateUtils.DATETIME_FORMAT) - .timeZone(DateTimeZone.forOffsetMillis(system.getDefaultTimeZone().getRawOffset())) - // ES dateHistogram bounds are inclusive while createdBefore parameter is exclusive - .extendedBounds(new ExtendedBounds(startInclusive ? startTime : (startTime + 1), endTime - 1L)); - addEffortAggregationIfNeeded(query, dateHistogram); - return Optional.of(dateHistogram); - } - - private Optional getMinCreatedAt(Map filters, QueryBuilder esQuery) { - String facetNameAndField = IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT; - SearchRequestBuilder esRequest = client - .prepareSearch(INDEX_TYPE_ISSUE) - .setSize(0); - BoolQueryBuilder esFilter = boolQuery(); - filters.values().stream().filter(Objects::nonNull).forEach(esFilter::must); - if (esFilter.hasClauses()) { - esRequest.setQuery(QueryBuilders.boolQuery().must(esQuery).filter(esFilter)); - } else { - esRequest.setQuery(esQuery); - } - esRequest.addAggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField)); - Min minValue = esRequest.get().getAggregations().get(facetNameAndField); - - Double actualValue = minValue.getValue(); - if (actualValue.isInfinite()) { - return Optional.empty(); - } - return Optional.of(actualValue.longValue()); - } - - private void addAssignedToMeFacetIfNeeded(SearchRequestBuilder builder, SearchOptions options, IssueQuery query, Map filters, QueryBuilder queryBuilder) { - String uuid = userSession.getUuid(); - - if (!options.getFacets().contains(FACET_ASSIGNED_TO_ME) || StringUtils.isEmpty(uuid)) { - return; - } - - String fieldName = IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID; - String facetName = FACET_ASSIGNED_TO_ME; - - // Same as in super.stickyFacetBuilder - StickyFacetBuilder assignedToMeFacetBuilder = newStickyFacetBuilder(query, filters, queryBuilder); - BoolQueryBuilder facetFilter = assignedToMeFacetBuilder.getStickyFacetFilter(IS_ASSIGNED_FILTER, fieldName); - - FilterAggregationBuilder facetTopAggregation = AggregationBuilders - .filter(facetName + "__filter", facetFilter) - .subAggregation(addEffortAggregationIfNeeded(query, AggregationBuilders.terms(facetName + "__terms") - .field(fieldName) - .includeExclude(new IncludeExclude(escapeSpecialRegexChars(uuid), null)))); - - builder.addAggregation( - AggregationBuilders.global(facetName) - .subAggregation(facetTopAggregation)); - } - - public List listTags(@Nullable OrganizationDto organization, @Nullable String textQuery, int size) { - int maxPageSize = 500; - checkArgument(size <= maxPageSize, "Page size must be lower than or equals to " + maxPageSize); - if (size <= 0) { - return emptyList(); - } - - BoolQueryBuilder esQuery = boolQuery() - .filter(createAuthorizationFilter(true)); - if (organization != null) { - esQuery.filter(termQuery(FIELD_ISSUE_ORGANIZATION_UUID, organization.getUuid())); - } - - SearchRequestBuilder requestBuilder = client - .prepareSearch(INDEX_TYPE_ISSUE) - .setQuery(esQuery) - .setSize(0); - - TermsAggregationBuilder termsAggregation = AggregationBuilders.terms(AGGREGATION_NAME_FOR_TAGS) - .field(IssueIndexDefinition.FIELD_ISSUE_TAGS) - .size(size) - .order(Terms.Order.term(true)) - .minDocCount(1L); - if (textQuery != null) { - String escapedTextQuery = escapeSpecialRegexChars(textQuery); - termsAggregation.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapedTextQuery), null)); - } - requestBuilder.addAggregation(termsAggregation); - - SearchResponse searchResponse = requestBuilder.get(); - Terms issuesResult = searchResponse.getAggregations().get(AGGREGATION_NAME_FOR_TAGS); - return EsUtils.termsKeys(issuesResult); - } - - public Map countTags(IssueQuery query, int maxNumberOfTags) { - Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_TAGS, query, null, Terms.Order.count(false), maxNumberOfTags); - return EsUtils.termsToMap(terms); - } - - public List listAuthors(IssueQuery query, @Nullable String textQuery, int maxNumberOfAuthors) { - Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query, textQuery, Terms.Order.term(true), maxNumberOfAuthors); - return EsUtils.termsKeys(terms); - } - - private Terms listTermsMatching(String fieldName, IssueQuery query, @Nullable String textQuery, Terms.Order termsOrder, int maxNumberOfTags) { - SearchRequestBuilder requestBuilder = client - .prepareSearch(INDEX_TYPE_ISSUE) - // Avoids returning search hits - .setSize(0); - - requestBuilder.setQuery(boolQuery().must(QueryBuilders.matchAllQuery()).filter(createBoolFilter(query))); - - TermsAggregationBuilder aggreg = AggregationBuilders.terms("_ref") - .field(fieldName) - .size(maxNumberOfTags) - .order(termsOrder) - .minDocCount(1L); - if (textQuery != null) { - aggreg.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapeSpecialRegexChars(textQuery)), null)); - } - - SearchResponse searchResponse = requestBuilder.addAggregation(aggreg).get(); - return searchResponse.getAggregations().get("_ref"); - } - - private BoolQueryBuilder createBoolFilter(IssueQuery query) { - BoolQueryBuilder boolQuery = boolQuery(); - for (QueryBuilder filter : createFilters(query).values()) { - if (filter != null) { - boolQuery.must(filter); - } - } - return boolQuery; - } - - public List searchProjectStatistics(List projectUuids, List froms, @Nullable String assigneeUuid) { - checkState(projectUuids.size() == froms.size(), - "Expected same size for projectUuids (had size %s) and froms (had size %s)", projectUuids.size(), froms.size()); - if (projectUuids.isEmpty()) { - return Collections.emptyList(); - } - SearchRequestBuilder request = client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) - .setQuery( - boolQuery() - .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)) - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, assigneeUuid)) - .mustNot(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))) - .setSize(0); - IntStream.range(0, projectUuids.size()).forEach(i -> { - String projectUuid = projectUuids.get(i); - long from = froms.get(i); - request - .addAggregation(AggregationBuilders - .filter(projectUuid, boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid)) - .filter(rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).gte(epochMillisToEpochSeconds(from)))) - .subAggregation( - AggregationBuilders.terms("branchUuid").field(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID) - .subAggregation( - AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY)) - .subAggregation( - AggregationBuilders.max("maxFuncCreatedAt").field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT)))); - }); - SearchResponse response = request.get(); - return response.getAggregations().asList().stream() - .map(x -> (InternalFilter) x) - .flatMap(projectBucket -> ((StringTerms) projectBucket.getAggregations().get("branchUuid")).getBuckets().stream() - .flatMap(branchBucket -> { - long count = ((InternalValueCount) branchBucket.getAggregations().get(COUNT)).getValue(); - if (count < 1L) { - return Stream.empty(); - } - long lastIssueDate = (long) ((InternalMax) branchBucket.getAggregations().get("maxFuncCreatedAt")).getValue(); - return Stream.of(new ProjectStatistics(branchBucket.getKeyAsString(), count, lastIssueDate)); - })) - .collect(MoreCollectors.toList(projectUuids.size())); - } - - public List searchBranchStatistics(String projectUuid, List branchUuids) { - if (branchUuids.isEmpty()) { - return Collections.emptyList(); - } - - SearchRequestBuilder request = client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) - .setRouting(projectUuid) - .setQuery( - boolQuery() - .must(termsQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, branchUuids)) - .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)) - .must(termQuery(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(false)))) - .setSize(0) - .addAggregation(AggregationBuilders.terms("branchUuids") - .field(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID) - .size(branchUuids.size()) - .subAggregation(AggregationBuilders.terms("types") - .field(IssueIndexDefinition.FIELD_ISSUE_TYPE))); - SearchResponse response = request.get(); - return ((StringTerms) response.getAggregations().get("branchUuids")).getBuckets().stream() - .map(bucket -> new BranchStatistics(bucket.getKeyAsString(), - ((StringTerms) bucket.getAggregations().get("types")).getBuckets() - .stream() - .collect(uniqueIndex(StringTerms.Bucket::getKeyAsString, InternalTerms.Bucket::getDocCount)))) - .collect(MoreCollectors.toList(branchUuids.size())); - } - - public List getSansTop25Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) { - SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); - Stream.of(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES).forEach(sansCategory -> { - AggregationBuilder sansCategoryAggs = AggregationBuilders - .filter(sansCategory, boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, sansCategory))); - request.addAggregation(addSecurityReportSubAggregations(sansCategoryAggs, includeCwe)); - }); - return processSecurityReportSearchResults(request, includeCwe); - } - - public List getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) { - SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); - Stream.concat(IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i), Stream.of(UNKNOWN_STANDARD)).forEach(owaspCategory -> { - AggregationBuilder owaspCategoryAggs = AggregationBuilders - .filter(owaspCategory, boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, owaspCategory))); - request.addAggregation(addSecurityReportSubAggregations(owaspCategoryAggs, includeCwe)); - }); - return processSecurityReportSearchResults(request, includeCwe); - } - - private static List processSecurityReportSearchResults(SearchRequestBuilder request, boolean includeCwe) { - SearchResponse response = request.get(); - return response.getAggregations().asList().stream() - .map(c -> processSecurityReportIssueSearchResults((InternalFilter) c, includeCwe)) - .collect(MoreCollectors.toList()); - } - - private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(InternalFilter categoryBucket, boolean includeCwe) { - List children = new ArrayList<>(); - - if (includeCwe) { - ((StringTerms) categoryBucket.getAggregations().get("cwe")).getBuckets() - .forEach(cweBucket -> children.add(processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null))); - } - - return processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getName(), children); - } - - private static SecurityStandardCategoryStatistics processSecurityReportCategorySearchResults(HasAggregations categoryBucket, String categoryName, - @Nullable List children) { - List severityBuckets = ((StringTerms) ((InternalFilter) categoryBucket.getAggregations().get("vulnerabilities")).getAggregations().get("severity")) - .getBuckets(); - long vulnerabilities = severityBuckets.stream().mapToLong(b -> ((InternalValueCount) b.getAggregations().get(COUNT)).getValue()).sum(); - // Worst severity having at least one issue - OptionalInt severityRating = severityBuckets.stream() - .filter(b -> ((InternalValueCount) b.getAggregations().get(COUNT)).getValue() != 0) - .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1) - .max(); - - long openSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("openSecurityHotspots")).getAggregations().get(COUNT)) - .getValue(); - long toReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("toReviewSecurityHotspots")).getAggregations().get(COUNT)) - .getValue(); - long wontFixSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("wontFixSecurityHotspots")).getAggregations().get(COUNT)) - .getValue(); - - return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots, openSecurityHotspots, - wontFixSecurityHotspots, children); - } - - private static AggregationBuilder addSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe) { - AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs); - if (includeCwe) { - categoriesAggs - .subAggregation(addSecurityReportIssueCountAggregations(AggregationBuilders.terms("cwe").field(IssueIndexDefinition.FIELD_ISSUE_CWE))); - } - return aggregationBuilder; - } - - private static AggregationBuilder addSecurityReportIssueCountAggregations(AggregationBuilder categoryAggs) { - return categoryAggs - .subAggregation( - AggregationBuilders.filter("vulnerabilities", boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.VULNERABILITY.name())) - .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))) - .subAggregation( - AggregationBuilders.terms("severity").field(IssueIndexDefinition.FIELD_ISSUE_SEVERITY) - .subAggregation( - AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY)))) - .subAggregation(AggregationBuilders.filter("openSecurityHotspots", boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) - .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))) - .subAggregation( - AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))) - .subAggregation(AggregationBuilders.filter("toReviewSecurityHotspots", boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_FIXED))) - .subAggregation( - AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))) - .subAggregation(AggregationBuilders.filter("wontFixSecurityHotspots", boolQuery() - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) - .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_WONT_FIX))) - .subAggregation( - AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))); - } - - private SearchRequestBuilder prepareNonClosedVulnerabilitiesAndHotspotSearch(String projectUuid, boolean isViewOrApp) { - BoolQueryBuilder componentFilter = boolQuery(); - if (isViewOrApp) { - componentFilter.filter(QueryBuilders.termsLookupQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, - new TermsLookup( - ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), - ViewIndexDefinition.INDEX_TYPE_VIEW.getType(), - projectUuid, - ViewIndexDefinition.FIELD_PROJECTS))); - } else { - componentFilter.filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, projectUuid)); - } - return client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) - .setQuery( - componentFilter - .filter(termsQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name(), RuleType.VULNERABILITY.name())) - .mustNot(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_CLOSED))) - .setSize(0); - } - -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java index dd13a01395a..d6e49f49a51 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIndexDefinition.java @@ -19,12 +19,17 @@ */ package org.sonar.server.issue.index; +import com.google.common.collect.ImmutableMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import org.sonar.api.config.Configuration; import org.sonar.api.config.internal.MapSettings; import org.sonar.server.es.IndexDefinition; import org.sonar.server.es.IndexType; import org.sonar.server.es.NewIndex; +import static java.util.Arrays.asList; import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; import static org.sonar.server.es.NewIndex.SettingsConfiguration.MANUAL_REFRESH_INTERVAL; import static org.sonar.server.es.NewIndex.SettingsConfiguration.newBuilder; @@ -95,6 +100,18 @@ public class IssueIndexDefinition implements IndexDefinition { public static final String FIELD_ISSUE_OWASP_TOP_10 = "owaspTop10"; public static final String FIELD_ISSUE_SANS_TOP_25 = "sansTop25"; public static final String FIELD_ISSUE_CWE = "cwe"; + public static final String UNKNOWN_STANDARD = "unknown"; + public static final String SANS_TOP_25_INSECURE_INTERACTION = "insecure-interaction"; + public static final String SANS_TOP_25_RISKY_RESOURCE = "risky-resource"; + public static final String SANS_TOP_25_POROUS_DEFENSES = "porous-defenses"; + // See https://www.sans.org/top25-software-errors + private static final Set INSECURE_CWE = new HashSet<>(asList("89", "78", "79", "434", "352", "601")); + private static final Set RISKY_CWE = new HashSet<>(asList("120", "22", "494", "829", "676", "131", "134", "190")); + private static final Set POROUS_CWE = new HashSet<>(asList("306", "862", "798", "311", "807", "250", "863", "732", "327", "307", "759")); + static final Map> SANS_TOP_25_CWE_MAPPING = ImmutableMap.of( + SANS_TOP_25_INSECURE_INTERACTION, INSECURE_CWE, + SANS_TOP_25_RISKY_RESOURCE, RISKY_CWE, + SANS_TOP_25_POROUS_DEFENSES, POROUS_CWE); private final Configuration config; private final boolean enableSource; diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java index 549974561f8..eafd2aed3b0 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/index/IssueIteratorForSingleChunk.java @@ -21,7 +21,6 @@ package org.sonar.server.issue.index; import com.google.common.base.CharMatcher; import com.google.common.base.Splitter; -import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterators; import com.google.common.collect.Maps; import java.sql.PreparedStatement; @@ -29,10 +28,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.IntStream; import javax.annotation.CheckForNull; @@ -46,14 +42,11 @@ import org.sonar.db.DbSession; import org.sonar.db.ResultSetIterator; import static com.google.common.base.Preconditions.checkArgument; -import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.sonar.api.utils.DateUtils.longToDate; import static org.sonar.db.DatabaseUtils.getLong; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_CWE_MAPPING; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; /** * Scrolls over table ISSUES and reads documents to populate @@ -106,15 +99,6 @@ class IssueIteratorForSingleChunk implements IssueIterator { private static final String OWASP_TOP10_PREFIX = "owaspTop10:"; private static final String CWE_PREFIX = "cwe:"; - // See https://www.sans.org/top25-software-errors - private static final Set INSECURE_CWE = new HashSet<>(asList("89", "78", "79", "434", "352", "601")); - private static final Set RISKY_CWE = new HashSet<>(asList("120", "22", "494", "829", "676", "131", "134", "190")); - private static final Set POROUS_CWE = new HashSet<>(asList("306", "862", "798", "311", "807", "250", "863", "732", "327", "307", "759")); - private static final Map> SANS_TOP_25_CWE_MAPPING = ImmutableMap.of( - SANS_TOP_25_INSECURE_INTERACTION, INSECURE_CWE, - SANS_TOP_25_RISKY_RESOURCE, RISKY_CWE, - SANS_TOP_25_POROUS_DEFENSES, POROUS_CWE); - private final DbSession session; @CheckForNull diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java deleted file mode 100644 index a759e0326c5..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java +++ /dev/null @@ -1,452 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Multimap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.IntStream; -import java.util.stream.Stream; -import javax.annotation.Nullable; -import org.apache.lucene.search.join.ScoreMode; -import org.elasticsearch.action.search.SearchRequestBuilder; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; -import org.elasticsearch.search.aggregations.AggregationBuilders; -import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; -import org.elasticsearch.search.aggregations.bucket.filter.Filter; -import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregator.KeyedFilter; -import org.elasticsearch.search.aggregations.bucket.nested.Nested; -import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.Terms; -import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; -import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; -import org.elasticsearch.search.aggregations.metrics.sum.Sum; -import org.elasticsearch.search.sort.FieldSortBuilder; -import org.sonar.api.ce.ComputeEngineSide; -import org.sonar.api.server.ServerSide; -import org.sonar.api.utils.System2; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.server.es.DefaultIndexSettingsElement; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.SearchIdResult; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.es.StickyFacetBuilder; -import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; -import org.sonar.server.permission.index.AuthorizationTypeSupport; - -import static com.google.common.base.Preconditions.checkArgument; -import static java.util.Collections.emptyList; -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.index.query.QueryBuilders.nestedQuery; -import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.elasticsearch.index.query.QueryBuilders.termsQuery; -import static org.elasticsearch.search.aggregations.AggregationBuilders.filters; -import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; -import static org.elasticsearch.search.sort.SortOrder.ASC; -import static org.elasticsearch.search.sort.SortOrder.DESC; -import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; -import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; -import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; -import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_LINES_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING_KEY; -import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY; -import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY; -import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY; -import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; -import static org.sonar.server.es.EsUtils.termsToMap; -import static org.sonar.server.measure.index.ProjectMeasuresDoc.QUALITY_GATE_STATUS; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ANALYSED_AT; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_LANGUAGES; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_MEASURES; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_LANGUAGE_DISTRIBUTION; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ORGANIZATION_UUID; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALITY_GATE_STATUS; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES; -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_TAGS; -import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE; - -@ServerSide -@ComputeEngineSide -public class ProjectMeasuresIndex { - - public static final List SUPPORTED_FACETS = ImmutableList.of( - NCLOC_KEY, - NEW_LINES_KEY, - DUPLICATED_LINES_DENSITY_KEY, - NEW_DUPLICATED_LINES_DENSITY_KEY, - COVERAGE_KEY, - NEW_COVERAGE_KEY, - SQALE_RATING_KEY, - NEW_MAINTAINABILITY_RATING_KEY, - RELIABILITY_RATING_KEY, - NEW_RELIABILITY_RATING_KEY, - SECURITY_RATING_KEY, - NEW_SECURITY_RATING_KEY, - ALERT_STATUS_KEY, - FILTER_LANGUAGES, - FILTER_TAGS); - - private static final Double[] LINES_THRESHOLDS = new Double[] {1_000d, 10_000d, 100_000d, 500_000d}; - private static final Double[] COVERAGE_THRESHOLDS = new Double[] {30d, 50d, 70d, 80d}; - private static final Double[] DUPLICATIONS_THRESHOLDS = new Double[] {3d, 5d, 10d, 20d}; - - private static final String FIELD_MEASURES_KEY = FIELD_MEASURES + "." + ProjectMeasuresIndexDefinition.FIELD_MEASURES_KEY; - private static final String FIELD_MEASURES_VALUE = FIELD_MEASURES + "." + ProjectMeasuresIndexDefinition.FIELD_MEASURES_VALUE; - private static final String FIELD_DISTRIB_LANGUAGE = FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "." + ProjectMeasuresIndexDefinition.FIELD_DISTRIB_LANGUAGE; - private static final String FIELD_DISTRIB_NCLOC = FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "." + ProjectMeasuresIndexDefinition.FIELD_DISTRIB_NCLOC; - - private static final Map FACET_FACTORIES = ImmutableMap.builder() - .put(NCLOC_KEY, (esSearch, query, facetBuilder) -> addRangeFacet(esSearch, NCLOC_KEY, facetBuilder, LINES_THRESHOLDS)) - .put(NEW_LINES_KEY, (esSearch, query, facetBuilder) -> addRangeFacet(esSearch, NEW_LINES_KEY, facetBuilder, LINES_THRESHOLDS)) - .put(DUPLICATED_LINES_DENSITY_KEY, - (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, DUPLICATED_LINES_DENSITY_KEY, facetBuilder, DUPLICATIONS_THRESHOLDS)) - .put(NEW_DUPLICATED_LINES_DENSITY_KEY, - (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, NEW_DUPLICATED_LINES_DENSITY_KEY, facetBuilder, DUPLICATIONS_THRESHOLDS)) - .put(COVERAGE_KEY, (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, COVERAGE_KEY, facetBuilder, COVERAGE_THRESHOLDS)) - .put(NEW_COVERAGE_KEY, (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, NEW_COVERAGE_KEY, facetBuilder, COVERAGE_THRESHOLDS)) - .put(SQALE_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, SQALE_RATING_KEY, facetBuilder)) - .put(NEW_MAINTAINABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_MAINTAINABILITY_RATING_KEY, facetBuilder)) - .put(RELIABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, RELIABILITY_RATING_KEY, facetBuilder)) - .put(NEW_RELIABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_RELIABILITY_RATING_KEY, facetBuilder)) - .put(SECURITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, SECURITY_RATING_KEY, facetBuilder)) - .put(NEW_SECURITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_SECURITY_RATING_KEY, facetBuilder)) - .put(ALERT_STATUS_KEY, (esSearch, query, facetBuilder) -> esSearch.addAggregation(createStickyFacet(ALERT_STATUS_KEY, facetBuilder, createQualityGateFacet()))) - .put(FILTER_LANGUAGES, ProjectMeasuresIndex::addLanguagesFacet) - .put(FIELD_TAGS, ProjectMeasuresIndex::addTagsFacet) - .build(); - - private final EsClient client; - private final AuthorizationTypeSupport authorizationTypeSupport; - private final System2 system2; - - public ProjectMeasuresIndex(EsClient client, AuthorizationTypeSupport authorizationTypeSupport, System2 system2) { - this.client = client; - this.authorizationTypeSupport = authorizationTypeSupport; - this.system2 = system2; - } - - public SearchIdResult search(ProjectMeasuresQuery query, SearchOptions searchOptions) { - SearchRequestBuilder requestBuilder = client - .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) - .setFetchSource(false) - .setFrom(searchOptions.getOffset()) - .setSize(searchOptions.getLimit()); - - BoolQueryBuilder esFilter = boolQuery(); - Map filters = createFilters(query); - filters.values().forEach(esFilter::must); - requestBuilder.setQuery(esFilter); - - addFacets(requestBuilder, searchOptions, filters, query); - addSort(query, requestBuilder); - return new SearchIdResult<>(requestBuilder.get(), id -> id, system2.getDefaultTimeZone()); - } - - public ProjectMeasuresStatistics searchTelemetryStatistics() { - SearchRequestBuilder request = client - .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) - .setFetchSource(false) - .setSize(0); - - BoolQueryBuilder esFilter = boolQuery(); - request.setQuery(esFilter); - request.addAggregation(AggregationBuilders.terms(FIELD_LANGUAGES) - .field(FIELD_LANGUAGES) - .size(MAX_PAGE_SIZE) - .minDocCount(1) - .order(Terms.Order.count(false))); - request.addAggregation(AggregationBuilders.nested(FIELD_NCLOC_LANGUAGE_DISTRIBUTION, FIELD_NCLOC_LANGUAGE_DISTRIBUTION) - .subAggregation(AggregationBuilders.terms(FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "_terms") - .field(FIELD_DISTRIB_LANGUAGE) - .size(MAX_PAGE_SIZE) - .minDocCount(1) - .order(Terms.Order.count(false)) - .subAggregation(sum(FIELD_DISTRIB_NCLOC).field(FIELD_DISTRIB_NCLOC)))); - - request.addAggregation(AggregationBuilders.nested(NCLOC_KEY, FIELD_MEASURES) - .subAggregation(AggregationBuilders.filter(NCLOC_KEY + "_filter", termQuery(FIELD_MEASURES_KEY, NCLOC_KEY)) - .subAggregation(sum(NCLOC_KEY + "_filter_sum").field(FIELD_MEASURES_VALUE)))); - - ProjectMeasuresStatistics.Builder statistics = ProjectMeasuresStatistics.builder(); - - SearchResponse response = request.get(); - statistics.setProjectCount(response.getHits().getTotalHits()); - Stream.of(NCLOC_KEY) - .map(metric -> (Nested) response.getAggregations().get(metric)) - .map(nested -> (Filter) nested.getAggregations().get(nested.getName() + "_filter")) - .map(filter -> (Sum) filter.getAggregations().get(filter.getName() + "_sum")) - .forEach(sum -> { - String metric = sum.getName().replace("_filter_sum", ""); - long value = Math.round(sum.getValue()); - statistics.setSum(metric, value); - }); - statistics.setProjectCountByLanguage(termsToMap(response.getAggregations().get(FIELD_LANGUAGES))); - Function bucketToNcloc = bucket -> Math.round(((Sum) bucket.getAggregations().get(FIELD_DISTRIB_NCLOC)).getValue()); - Map nclocByLanguage = Stream.of((Nested) response.getAggregations().get(FIELD_NCLOC_LANGUAGE_DISTRIBUTION)) - .map(nested -> (Terms) nested.getAggregations().get(nested.getName() + "_terms")) - .flatMap(terms -> terms.getBuckets().stream()) - .collect(MoreCollectors.uniqueIndex(Bucket::getKeyAsString, bucketToNcloc)); - statistics.setNclocByLanguage(nclocByLanguage); - - return statistics.build(); - } - - private static void addSort(ProjectMeasuresQuery query, SearchRequestBuilder requestBuilder) { - String sort = query.getSort(); - if (SORT_BY_NAME.equals(sort)) { - requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), query.isAsc() ? ASC : DESC); - } else if (SORT_BY_LAST_ANALYSIS_DATE.equals(sort)) { - requestBuilder.addSort(FIELD_ANALYSED_AT, query.isAsc() ? ASC : DESC); - } else if (ALERT_STATUS_KEY.equals(sort)) { - requestBuilder.addSort(FIELD_QUALITY_GATE_STATUS, query.isAsc() ? ASC : DESC); - requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC); - } else { - addMetricSort(query, requestBuilder, sort); - requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC); - } - // last sort is by key in order to be deterministic when same value - requestBuilder.addSort(FIELD_KEY, ASC); - } - - private static void addMetricSort(ProjectMeasuresQuery query, SearchRequestBuilder requestBuilder, String sort) { - requestBuilder.addSort( - new FieldSortBuilder(FIELD_MEASURES_VALUE) - .setNestedPath(FIELD_MEASURES) - .setNestedFilter(termQuery(FIELD_MEASURES_KEY, sort)) - .order(query.isAsc() ? ASC : DESC)); - } - - private static void addRangeFacet(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder, Double... thresholds) { - esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, createRangeFacet(metricKey, thresholds))); - } - - private static void addRangeFacetIncludingNoData(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder, Double... thresholds) { - esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, - AggregationBuilders.filter("combined_" + metricKey, matchAllQuery()) - .subAggregation(createRangeFacet(metricKey, thresholds)) - .subAggregation(createNoDataFacet(metricKey)))); - } - - private static void addRatingFacet(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder) { - esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, createRatingFacet(metricKey))); - } - - private static void addLanguagesFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder) { - esSearch.addAggregation(facetBuilder.buildStickyFacet(FIELD_LANGUAGES, FILTER_LANGUAGES, query.getLanguages().map(Set::toArray).orElseGet(() -> new Object[] {}))); - } - - private static void addTagsFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder) { - esSearch.addAggregation(facetBuilder.buildStickyFacet(FIELD_TAGS, FILTER_TAGS, query.getTags().map(Set::toArray).orElseGet(() -> new Object[] {}))); - } - - private static void addFacets(SearchRequestBuilder esSearch, SearchOptions options, Map filters, ProjectMeasuresQuery query) { - StickyFacetBuilder facetBuilder = new StickyFacetBuilder(matchAllQuery(), filters); - options.getFacets().stream() - .filter(FACET_FACTORIES::containsKey) - .map(FACET_FACTORIES::get) - .forEach(factory -> factory.addFacet(esSearch, query, facetBuilder)); - } - - private static AbstractAggregationBuilder createStickyFacet(String facetKey, StickyFacetBuilder facetBuilder, AbstractAggregationBuilder aggregationBuilder) { - BoolQueryBuilder facetFilter = facetBuilder.getStickyFacetFilter(facetKey); - return AggregationBuilders - .global(facetKey) - .subAggregation( - AggregationBuilders - .filter("facet_filter_" + facetKey, facetFilter) - .subAggregation(aggregationBuilder)); - } - - private static AbstractAggregationBuilder createRangeFacet(String metricKey, Double... thresholds) { - RangeAggregationBuilder rangeAgg = AggregationBuilders.range(metricKey) - .field(FIELD_MEASURES_VALUE); - final int lastIndex = thresholds.length - 1; - IntStream.range(0, thresholds.length) - .forEach(i -> { - if (i == 0) { - rangeAgg.addUnboundedTo(thresholds[0]); - rangeAgg.addRange(thresholds[0], thresholds[1]); - } else if (i == lastIndex) { - rangeAgg.addUnboundedFrom(thresholds[lastIndex]); - } else { - rangeAgg.addRange(thresholds[i], thresholds[i + 1]); - } - }); - - return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES) - .subAggregation( - AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_KEY, metricKey)) - .subAggregation(rangeAgg)); - } - - private static AbstractAggregationBuilder createNoDataFacet(String metricKey) { - return AggregationBuilders.filter( - "no_data_" + metricKey, - boolQuery().mustNot(nestedQuery(FIELD_MEASURES, termQuery(FIELD_MEASURES_KEY, metricKey), ScoreMode.Avg))); - } - - private static AbstractAggregationBuilder createRatingFacet(String metricKey) { - return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES) - .subAggregation( - AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_KEY, metricKey)) - .subAggregation(filters(metricKey, - new KeyedFilter("1", termQuery(FIELD_MEASURES_VALUE, 1d)), - new KeyedFilter("2", termQuery(FIELD_MEASURES_VALUE, 2d)), - new KeyedFilter("3", termQuery(FIELD_MEASURES_VALUE, 3d)), - new KeyedFilter("4", termQuery(FIELD_MEASURES_VALUE, 4d)), - new KeyedFilter("5", termQuery(FIELD_MEASURES_VALUE, 5d))))); - } - - private static AbstractAggregationBuilder createQualityGateFacet() { - return AggregationBuilders.filters( - ALERT_STATUS_KEY, - QUALITY_GATE_STATUS.entrySet().stream() - .map(entry -> new KeyedFilter(entry.getKey(), termQuery(FIELD_QUALITY_GATE_STATUS, entry.getValue()))) - .toArray(KeyedFilter[]::new)); - } - - private Map createFilters(ProjectMeasuresQuery query) { - Map filters = new HashMap<>(); - filters.put("__authorization", authorizationTypeSupport.createQueryFilter()); - Multimap metricCriterionMultimap = ArrayListMultimap.create(); - query.getMetricCriteria().forEach(metricCriterion -> metricCriterionMultimap.put(metricCriterion.getMetricKey(), metricCriterion)); - metricCriterionMultimap.asMap().forEach((key, value) -> { - BoolQueryBuilder metricFilters = boolQuery(); - value - .stream() - .map(ProjectMeasuresIndex::toQuery) - .forEach(metricFilters::must); - filters.put(key, metricFilters); - }); - - query.getQualityGateStatus() - .ifPresent(qualityGateStatus -> filters.put(ALERT_STATUS_KEY, termQuery(FIELD_QUALITY_GATE_STATUS, QUALITY_GATE_STATUS.get(qualityGateStatus.name())))); - - query.getProjectUuids() - .ifPresent(projectUuids -> filters.put("ids", termsQuery("_id", projectUuids))); - - query.getLanguages() - .ifPresent(languages -> filters.put(FILTER_LANGUAGES, termsQuery(FIELD_LANGUAGES, languages))); - - query.getOrganizationUuid() - .ifPresent(organizationUuid -> filters.put(FIELD_ORGANIZATION_UUID, termQuery(FIELD_ORGANIZATION_UUID, organizationUuid))); - - query.getTags() - .ifPresent(tags -> filters.put(FIELD_TAGS, termsQuery(FIELD_TAGS, tags))); - - query.getQueryText() - .map(ProjectsTextSearchQueryFactory::createQuery) - .ifPresent(queryBuilder -> filters.put("textQuery", queryBuilder)); - return filters; - } - - private static QueryBuilder toQuery(MetricCriterion criterion) { - if (criterion.isNoData()) { - return boolQuery().mustNot( - nestedQuery( - FIELD_MEASURES, - termQuery(FIELD_MEASURES_KEY, criterion.getMetricKey()), - ScoreMode.Avg)); - } - return nestedQuery( - FIELD_MEASURES, - boolQuery() - .filter(termQuery(FIELD_MEASURES_KEY, criterion.getMetricKey())) - .filter(toValueQuery(criterion)), - ScoreMode.Avg); - } - - private static QueryBuilder toValueQuery(MetricCriterion criterion) { - String fieldName = FIELD_MEASURES_VALUE; - - switch (criterion.getOperator()) { - case GT: - return rangeQuery(fieldName).gt(criterion.getValue()); - case GTE: - return rangeQuery(fieldName).gte(criterion.getValue()); - case LT: - return rangeQuery(fieldName).lt(criterion.getValue()); - case LTE: - return rangeQuery(fieldName).lte(criterion.getValue()); - case EQ: - return termQuery(fieldName, criterion.getValue()); - default: - throw new IllegalStateException("Metric criteria non supported: " + criterion.getOperator().name()); - } - } - - public List searchTags(@Nullable String textQuery, int size) { - int maxPageSize = 500; - checkArgument(size <= maxPageSize, "Page size must be lower than or equals to " + maxPageSize); - if (size <= 0) { - return emptyList(); - } - - TermsAggregationBuilder tagFacet = AggregationBuilders.terms(FIELD_TAGS) - .field(FIELD_TAGS) - .size(size) - .minDocCount(1) - .order(Terms.Order.term(true)); - if (textQuery != null) { - tagFacet.includeExclude(new IncludeExclude(".*" + escapeSpecialRegexChars(textQuery) + ".*", null)); - } - - SearchRequestBuilder searchQuery = client - .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) - .setQuery(authorizationTypeSupport.createQueryFilter()) - .setFetchSource(false) - .setSize(0) - .addAggregation(tagFacet); - - Terms aggregation = searchQuery.get().getAggregations().get(FIELD_TAGS); - - return aggregation.getBuckets().stream() - .map(Bucket::getKeyAsString) - .collect(MoreCollectors.toList()); - } - - @FunctionalInterface - private interface FacetSetter { - void addFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder); - } - -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java deleted file mode 100644 index 17494bc8089..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import javax.annotation.Nullable; -import org.sonar.api.measures.Metric; - -import static com.google.common.base.Preconditions.checkState; -import static java.lang.String.format; -import static java.util.Arrays.stream; -import static java.util.Objects.requireNonNull; - -public class ProjectMeasuresQuery { - - public static final String SORT_BY_NAME = "name"; - public static final String SORT_BY_LAST_ANALYSIS_DATE = "analysisDate"; - - private List metricCriteria = new ArrayList<>(); - private Metric.Level qualityGateStatus; - private String organizationUuid; - private Set projectUuids; - private Set languages; - private Set tags; - private String sort = SORT_BY_NAME; - private boolean asc = true; - private String queryText; - - public ProjectMeasuresQuery addMetricCriterion(MetricCriterion metricCriterion) { - this.metricCriteria.add(metricCriterion); - return this; - } - - public List getMetricCriteria() { - return metricCriteria; - } - - public ProjectMeasuresQuery setQualityGateStatus(Metric.Level qualityGateStatus) { - this.qualityGateStatus = requireNonNull(qualityGateStatus); - return this; - } - - public Optional getQualityGateStatus() { - return Optional.ofNullable(qualityGateStatus); - } - - public ProjectMeasuresQuery setOrganizationUuid(@Nullable String organizationUuid) { - this.organizationUuid = organizationUuid; - return this; - } - - public Optional getOrganizationUuid() { - return Optional.ofNullable(organizationUuid); - } - - public ProjectMeasuresQuery setProjectUuids(@Nullable Set projectUuids) { - this.projectUuids = projectUuids; - return this; - } - - public Optional> getProjectUuids() { - return Optional.ofNullable(projectUuids); - } - - public ProjectMeasuresQuery setLanguages(@Nullable Set languages) { - this.languages = languages; - return this; - } - - public Optional> getLanguages() { - return Optional.ofNullable(languages); - } - - public ProjectMeasuresQuery setTags(@Nullable Set tags) { - this.tags = tags; - return this; - } - - public Optional> getTags() { - return Optional.ofNullable(tags); - } - - public Optional getQueryText() { - return Optional.ofNullable(queryText); - } - - public ProjectMeasuresQuery setQueryText(@Nullable String queryText) { - this.queryText = queryText; - return this; - } - - public String getSort() { - return sort; - } - - public ProjectMeasuresQuery setSort(String sort) { - this.sort = requireNonNull(sort, "Sort cannot be null"); - return this; - } - - public boolean isAsc() { - return asc; - } - - public ProjectMeasuresQuery setAsc(boolean asc) { - this.asc = asc; - return this; - } - - public static class MetricCriterion { - private final String metricKey; - private final Operator operator; - @Nullable - private final Double value; - - private MetricCriterion(String metricKey, @Nullable Operator operator, @Nullable Double value) { - this.metricKey = metricKey; - this.operator = operator; - this.value = value; - } - - public String getMetricKey() { - return metricKey; - } - - public Operator getOperator() { - checkDataAvailable(); - return operator; - } - - public double getValue() { - checkDataAvailable(); - return value; - } - - public boolean isNoData() { - return value == null; - } - - public static MetricCriterion createNoData(String metricKey) { - return new MetricCriterion(requireNonNull(metricKey), null, null); - } - - public static MetricCriterion create(String metricKey, Operator operator, double value) { - return new MetricCriterion(requireNonNull(metricKey), requireNonNull(operator), value); - } - - private void checkDataAvailable() { - checkState(!isNoData(), "The criterion for metric %s has no data", metricKey); - } - } - - public enum Operator { - LT("<"), LTE("<="), GT(">"), GTE(">="), EQ("="), IN("in"); - - String value; - - Operator(String value) { - this.value = value; - } - - public String getValue() { - return value; - } - - public static Operator getByValue(String value) { - return stream(Operator.values()) - .filter(operator -> operator.getValue().equalsIgnoreCase(value)) - .findFirst() - .orElseThrow(() -> new IllegalArgumentException(format("Unknown operator '%s'", value))); - } - } - -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java deleted file mode 100644 index bf6cc6c5234..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import org.sonar.core.platform.Module; - -public class ProjectsEsModule extends Module { - @Override - protected void configureModule() { - add( - ProjectMeasuresIndexDefinition.class, - ProjectMeasuresIndex.class, - ProjectMeasuresIndexer.class); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java deleted file mode 100644 index 71456795254..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import java.util.Arrays; -import java.util.Locale; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.stream.Stream; -import org.apache.commons.lang.StringUtils; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.MatchQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.sonar.server.es.DefaultIndexSettings; - -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.matchQuery; -import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; -import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_GRAMS_ANALYZER; -import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; - -/** - * This class is used in order to do some advanced full text search on projects key and name - */ -class ProjectsTextSearchQueryFactory { - - private ProjectsTextSearchQueryFactory() { - // Only static methods - } - - static QueryBuilder createQuery(String queryText) { - BoolQueryBuilder featureQuery = boolQuery(); - Arrays.stream(ComponentTextSearchFeature.values()) - .map(f -> f.getQuery(queryText)) - .forEach(featureQuery::should); - return featureQuery; - } - - private enum ComponentTextSearchFeature { - - EXACT_IGNORE_CASE { - @Override - QueryBuilder getQuery(String queryText) { - return matchQuery(SORTABLE_ANALYZER.subField(FIELD_NAME), queryText) - .boost(2.5f); - } - }, - PREFIX { - @Override - QueryBuilder getQuery(String queryText) { - return prefixAndPartialQuery(queryText, FIELD_NAME, FIELD_NAME) - .boost(2f); - } - }, - PREFIX_IGNORE_CASE { - @Override - QueryBuilder getQuery(String queryText) { - String lowerCaseQueryText = queryText.toLowerCase(Locale.ENGLISH); - return prefixAndPartialQuery(lowerCaseQueryText, SORTABLE_ANALYZER.subField(FIELD_NAME), FIELD_NAME) - .boost(3f); - } - }, - PARTIAL { - @Override - QueryBuilder getQuery(String queryText) { - BoolQueryBuilder queryBuilder = boolQuery(); - split(queryText) - .map(text -> partialTermQuery(text, FIELD_NAME)) - .forEach(queryBuilder::must); - return queryBuilder - .boost(0.5f); - } - }, - KEY { - @Override - QueryBuilder getQuery(String queryText) { - return matchQuery(SORTABLE_ANALYZER.subField(FIELD_KEY), queryText) - .boost(50f); - } - }; - - abstract QueryBuilder getQuery(String queryText); - - protected Stream split(String queryText) { - return Arrays.stream( - queryText.split(DefaultIndexSettings.SEARCH_TERM_TOKENIZER_PATTERN)) - .filter(StringUtils::isNotEmpty); - } - - protected BoolQueryBuilder prefixAndPartialQuery(String queryText, String fieldName, String originalFieldName) { - BoolQueryBuilder queryBuilder = boolQuery(); - AtomicBoolean first = new AtomicBoolean(true); - split(queryText) - .map(queryTerm -> { - if (first.getAndSet(false)) { - return prefixQuery(fieldName, queryTerm); - } - return partialTermQuery(queryTerm, originalFieldName); - }) - .forEach(queryBuilder::must); - return queryBuilder; - } - - protected MatchQueryBuilder partialTermQuery(String queryTerm, String fieldName) { - // We will truncate the search to the maximum length of nGrams in the index. - // Otherwise the search would for sure not find any results. - String truncatedQuery = StringUtils.left(queryTerm, DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH); - return matchQuery(SEARCH_GRAMS_ANALYZER.subField(fieldName), truncatedQuery); - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationScope.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationScope.java index a4f28e6a332..efec4949f82 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationScope.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationScope.java @@ -23,18 +23,30 @@ import java.util.function.Predicate; import javax.annotation.concurrent.Immutable; import org.sonar.server.es.IndexType; +import static com.google.common.base.Preconditions.checkArgument; import static java.util.Objects.requireNonNull; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; @Immutable public final class AuthorizationScope { private final IndexType indexType; - private final Predicate projectPredicate; + private final Predicate projectPredicate; - public AuthorizationScope(IndexType indexType, Predicate projectPredicate) { - this.indexType = AuthorizationTypeSupport.getAuthorizationIndexType(indexType); + public AuthorizationScope(IndexType indexType, Predicate projectPredicate) { + this.indexType = getAuthorizationIndexType(indexType); this.projectPredicate = requireNonNull(projectPredicate); } + /** + * @return the identifier of the ElasticSearch type (including it's index name), that corresponds to a certain document type + */ + private static IndexType getAuthorizationIndexType(IndexType indexType) { + requireNonNull(indexType); + requireNonNull(indexType.getIndex()); + checkArgument(!TYPE_AUTHORIZATION.equals(indexType.getType()), "Authorization types do not have authorization on their own."); + return new IndexType(indexType.getIndex(), TYPE_AUTHORIZATION); + } + /** * Identifier of the authorization type (in the same index than the original IndexType, passed into the constructor). */ @@ -43,10 +55,9 @@ public final class AuthorizationScope { } /** - * Predicates that filters the projects to be involved in - * authorization. + * Predicates that filters the projects to be involved in authorization. */ - public Predicate getProjectPredicate() { + public Predicate getProjectPredicate() { return projectPredicate; } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java deleted file mode 100644 index 3abdfae4cc2..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/AuthorizationTypeSupport.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import com.google.common.collect.ImmutableMap; -import java.util.Optional; -import org.elasticsearch.index.query.BoolQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.join.query.JoinQueryBuilders; -import org.sonar.api.ce.ComputeEngineSide; -import org.sonar.api.server.ServerSide; -import org.sonar.db.user.GroupDto; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.NewIndex; -import org.sonar.server.user.UserSession; - -import static com.google.common.base.Preconditions.checkArgument; -import static java.util.Objects.requireNonNull; -import static org.elasticsearch.index.query.QueryBuilders.boolQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; - -@ServerSide -@ComputeEngineSide -public class AuthorizationTypeSupport { - - public static final String TYPE_AUTHORIZATION = "authorization"; - public static final String FIELD_GROUP_IDS = "groupIds"; - public static final String FIELD_USER_IDS = "userIds"; - - /** - * When true, then anybody can access to the project. In that case - * it's useless to store granted groups and users. The related - * fields are empty. - */ - public static final String FIELD_ALLOW_ANYONE = "allowAnyone"; - - private final UserSession userSession; - - public AuthorizationTypeSupport(UserSession userSession) { - this.userSession = userSession; - } - - /** - * @return the identifier of the ElasticSearch type (including it's index name), that corresponds to a certain document type - */ - public static IndexType getAuthorizationIndexType(IndexType indexType) { - requireNonNull(indexType); - requireNonNull(indexType.getIndex()); - checkArgument(!AuthorizationTypeSupport.TYPE_AUTHORIZATION.equals(indexType.getType()), "Authorization types do not have authorization on their own."); - return new IndexType(indexType.getIndex(), AuthorizationTypeSupport.TYPE_AUTHORIZATION); - } - - /** - * Creates a type that requires to verify that user has the read permission - * when searching for documents. - * It relies on a parent type named "authorization" that is automatically - * populated by {@link org.sonar.server.permission.index.PermissionIndexer}. - * - * Both types {@code typeName} and "authorization" are created. Documents - * must be created with _parent and _routing having the parent uuid as values. - * - * @see NewIndex.NewIndexType#requireProjectAuthorization() - */ - public static NewIndex.NewIndexType enableProjectAuthorization(NewIndex.NewIndexType type) { - type.setAttribute("_parent", ImmutableMap.of("type", TYPE_AUTHORIZATION)); - type.setAttribute("_routing", ImmutableMap.of("required", true)); - - NewIndex.NewIndexType authType = type.getIndex().createType(TYPE_AUTHORIZATION); - authType.setAttribute("_routing", ImmutableMap.of("required", true)); - authType.createLongField(FIELD_GROUP_IDS); - authType.createLongField(FIELD_USER_IDS); - authType.createBooleanField(FIELD_ALLOW_ANYONE); - authType.setEnableSource(false); - return type; - } - - /** - * Build a filter to restrict query to the documents on which - * user has read access. - */ - public QueryBuilder createQueryFilter() { - if (userSession.isRoot()) { - return QueryBuilders.matchAllQuery(); - } - - Integer userId = userSession.getUserId(); - BoolQueryBuilder filter = boolQuery(); - - // anyone - filter.should(QueryBuilders.termQuery(FIELD_ALLOW_ANYONE, true)); - - // users - Optional.ofNullable(userId) - .map(Integer::longValue) - .ifPresent(id -> filter.should(termQuery(FIELD_USER_IDS, id))); - - // groups - userSession.getGroups() - .stream() - .map(GroupDto::getId) - .forEach(groupId -> filter.should(termQuery(FIELD_GROUP_IDS, groupId))); - - return JoinQueryBuilders.hasParentQuery( - TYPE_AUTHORIZATION, - QueryBuilders.boolQuery().filter(filter), - false); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexAuthorizationConstants.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexAuthorizationConstants.java new file mode 100644 index 00000000000..416f66f768d --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexAuthorizationConstants.java @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +public final class IndexAuthorizationConstants { + public static final String TYPE_AUTHORIZATION = "authorization"; + public static final String FIELD_GROUP_IDS = "groupIds"; + public static final String FIELD_USER_IDS = "userIds"; + /** + * When true, then anybody can access to the project. In that case + * it's useless to store granted groups and users. The related + * fields are empty. + */ + public static final String FIELD_ALLOW_ANYONE = "allowAnyone"; + + private IndexAuthorizationConstants() { + // prevents instantiation + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexPermissions.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexPermissions.java new file mode 100644 index 00000000000..effa29bb462 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/IndexPermissions.java @@ -0,0 +1,70 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.ArrayList; +import java.util.List; + +public final class IndexPermissions { + private final String projectUuid; + private final String qualifier; + private final List userIds = new ArrayList<>(); + private final List groupIds = new ArrayList<>(); + private boolean allowAnyone = false; + + public IndexPermissions(String projectUuid, String qualifier) { + this.projectUuid = projectUuid; + this.qualifier = qualifier; + } + + public String getProjectUuid() { + return projectUuid; + } + + public String getQualifier() { + return qualifier; + } + + public List getUserIds() { + return userIds; + } + + public IndexPermissions addUserId(int l) { + userIds.add(l); + return this; + } + + public IndexPermissions addGroupId(int id) { + groupIds.add(id); + return this; + } + + public List getGroupIds() { + return groupIds; + } + + public void allowAnyone() { + this.allowAnyone = true; + } + + public boolean isAllowAnyone() { + return allowAnyone; + } +} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/NeedAuthorizationIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/NeedAuthorizationIndexer.java index a240fcc3580..0ce996c34f5 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/NeedAuthorizationIndexer.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/NeedAuthorizationIndexer.java @@ -22,7 +22,7 @@ package org.sonar.server.permission.index; /** * An {@link NeedAuthorizationIndexer} defines how * a {@link org.sonar.server.es.ProjectIndexer} populates - * the type named {@link AuthorizationTypeSupport#TYPE_AUTHORIZATION}, which + * the type named {@link WebAuthorizationTypeSupport#TYPE_AUTHORIZATION}, which * is used to verify that a user can access to projects. */ public interface NeedAuthorizationIndexer { diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java deleted file mode 100644 index 1a9ab35efec..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import com.google.common.annotations.VisibleForTesting; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.elasticsearch.action.index.IndexRequest; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.es.EsQueueDto; -import org.sonar.server.es.BulkIndexer; -import org.sonar.server.es.BulkIndexer.Size; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.IndexingResult; -import org.sonar.server.es.OneToOneResilientIndexingListener; -import org.sonar.server.es.ProjectIndexer; -import org.sonar.server.permission.index.PermissionIndexerDao.Dto; - -import static java.util.Collections.emptyList; -import static org.sonar.core.util.stream.MoreCollectors.toArrayList; -import static org.sonar.core.util.stream.MoreCollectors.toSet; - -/** - * Populates the types "authorization" of each index requiring project - * authorization. - */ -public class PermissionIndexer implements ProjectIndexer { - - private final DbClient dbClient; - private final EsClient esClient; - private final Collection authorizationScopes; - private final Set indexTypes; - - public PermissionIndexer(DbClient dbClient, EsClient esClient, NeedAuthorizationIndexer... needAuthorizationIndexers) { - this(dbClient, esClient, Arrays.stream(needAuthorizationIndexers) - .map(NeedAuthorizationIndexer::getAuthorizationScope) - .collect(MoreCollectors.toList(needAuthorizationIndexers.length))); - } - - @VisibleForTesting - public PermissionIndexer(DbClient dbClient, EsClient esClient, Collection authorizationScopes) { - this.dbClient = dbClient; - this.esClient = esClient; - this.authorizationScopes = authorizationScopes; - this.indexTypes = authorizationScopes.stream() - .map(AuthorizationScope::getIndexType) - .collect(toSet(authorizationScopes.size())); - } - - @Override - public Set getIndexTypes() { - return indexTypes; - } - - @Override - public void indexOnStartup(Set uninitializedIndexTypes) { - // TODO do not load everything in memory. Db rows should be scrolled. - List authorizations = getAllAuthorizations(); - Stream scopes = getScopes(uninitializedIndexTypes); - index(authorizations, scopes, Size.LARGE); - } - - @VisibleForTesting - void index(List authorizations) { - index(authorizations, authorizationScopes.stream(), Size.REGULAR); - } - - @Override - public void indexOnAnalysis(String branchUuid) { - // nothing to do, permissions don't change during an analysis - } - - @Override - public Collection prepareForRecovery(DbSession dbSession, Collection projectUuids, ProjectIndexer.Cause cause) { - switch (cause) { - case MEASURE_CHANGE: - case PROJECT_KEY_UPDATE: - case PROJECT_TAGS_UPDATE: - // nothing to change. Measures, project key and tags are not part of this index - return emptyList(); - - case PROJECT_CREATION: - case PROJECT_DELETION: - case PERMISSION_CHANGE: - return insertIntoEsQueue(dbSession, projectUuids); - - default: - // defensive case - throw new IllegalStateException("Unsupported cause: " + cause); - } - } - - private Collection insertIntoEsQueue(DbSession dbSession, Collection projectUuids) { - List items = indexTypes.stream() - .flatMap(indexType -> projectUuids.stream().map(projectUuid -> EsQueueDto.create(indexType.format(), projectUuid, null, projectUuid))) - .collect(toArrayList()); - - dbClient.esQueueDao().insert(dbSession, items); - return items; - } - - private void index(Collection authorizations, Stream scopes, Size bulkSize) { - if (authorizations.isEmpty()) { - return; - } - - // index each authorization in each scope - scopes.forEach(scope -> { - IndexType indexType = scope.getIndexType(); - - BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType, bulkSize); - bulkIndexer.start(); - - authorizations.stream() - .filter(scope.getProjectPredicate()) - .map(dto -> newIndexRequest(dto, indexType)) - .forEach(bulkIndexer::add); - - bulkIndexer.stop(); - }); - } - - @Override - public IndexingResult index(DbSession dbSession, Collection items) { - IndexingResult result = new IndexingResult(); - - List bulkIndexers = items.stream() - .map(EsQueueDto::getDocType) - .distinct() - .map(IndexType::parse) - .filter(indexTypes::contains) - .map(indexType -> new BulkIndexer(esClient, indexType, Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items))) - .collect(Collectors.toList()); - - if (bulkIndexers.isEmpty()) { - return result; - } - - bulkIndexers.forEach(BulkIndexer::start); - - PermissionIndexerDao permissionIndexerDao = new PermissionIndexerDao(); - Set remainingProjectUuids = items.stream().map(EsQueueDto::getDocId).collect(MoreCollectors.toHashSet()); - permissionIndexerDao.selectByUuids(dbClient, dbSession, remainingProjectUuids).forEach(p -> { - remainingProjectUuids.remove(p.getProjectUuid()); - bulkIndexers.forEach(bi -> bi.add(newIndexRequest(p, bi.getIndexType()))); - }); - - // the remaining references on projects that don't exist in db. They must - // be deleted from index. - remainingProjectUuids.forEach(projectUuid -> bulkIndexers.forEach(bi -> bi.addDeletion(bi.getIndexType(), projectUuid, projectUuid))); - - bulkIndexers.forEach(b -> result.add(b.stop())); - - return result; - } - - private static IndexRequest newIndexRequest(PermissionIndexerDao.Dto dto, IndexType indexType) { - Map doc = new HashMap<>(); - if (dto.isAllowAnyone()) { - doc.put(AuthorizationTypeSupport.FIELD_ALLOW_ANYONE, true); - // no need to feed users and groups - } else { - doc.put(AuthorizationTypeSupport.FIELD_ALLOW_ANYONE, false); - doc.put(AuthorizationTypeSupport.FIELD_GROUP_IDS, dto.getGroupIds()); - doc.put(AuthorizationTypeSupport.FIELD_USER_IDS, dto.getUserIds()); - } - return new IndexRequest(indexType.getIndex(), indexType.getType()) - .id(dto.getProjectUuid()) - .routing(dto.getProjectUuid()) - .source(doc); - } - - private Stream getScopes(Set indexTypes) { - return authorizationScopes.stream() - .filter(scope -> indexTypes.contains(scope.getIndexType())); - } - - private List getAllAuthorizations() { - try (DbSession dbSession = dbClient.openSession(false)) { - return new PermissionIndexerDao().selectAll(dbClient, dbSession); - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java deleted file mode 100644 index 5f73a4a3390..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import com.google.common.collect.ImmutableList; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang.StringUtils; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; - -import static org.apache.commons.lang.StringUtils.repeat; -import static org.sonar.db.DatabaseUtils.executeLargeInputs; - -/** - * No streaming because of union of joins -> no need to use ResultSetIterator - */ -public class PermissionIndexerDao { - - public static final class Dto { - private final String projectUuid; - private final String qualifier; - private final List userIds = new ArrayList<>(); - private final List groupIds = new ArrayList<>(); - private boolean allowAnyone = false; - - public Dto(String projectUuid, String qualifier) { - this.projectUuid = projectUuid; - this.qualifier = qualifier; - } - - public String getProjectUuid() { - return projectUuid; - } - - public String getQualifier() { - return qualifier; - } - - public List getUserIds() { - return userIds; - } - - public Dto addUserId(int l) { - userIds.add(l); - return this; - } - - public Dto addGroupId(int id) { - groupIds.add(id); - return this; - } - - public List getGroupIds() { - return groupIds; - } - - public void allowAnyone() { - this.allowAnyone = true; - } - - public boolean isAllowAnyone() { - return allowAnyone; - } - } - - private enum RowKind { - USER, GROUP, ANYONE, NONE - } - - private static final String SQL_TEMPLATE = "SELECT " + - " project_authorization.kind as kind, " + - " project_authorization.project as project, " + - " project_authorization.user_id as user_id, " + - " project_authorization.group_id as group_id, " + - " project_authorization.qualifier as qualifier " + - "FROM ( " + - - // users - - " SELECT '" + RowKind.USER + "' as kind," + - " projects.uuid AS project, " + - " projects.qualifier AS qualifier, " + - " user_roles.user_id AS user_id, " + - " NULL AS group_id " + - " FROM projects " + - " INNER JOIN user_roles ON user_roles.resource_id = projects.id AND user_roles.role = 'user' " + - " WHERE " + - " (projects.qualifier = 'TRK' " + - " or projects.qualifier = 'VW' " + - " or projects.qualifier = 'APP') " + - " AND projects.copy_component_uuid is NULL " + - " {projectsCondition} " + - " UNION " + - - // groups - - " SELECT '" + RowKind.GROUP + "' as kind," + - " projects.uuid AS project, " + - " projects.qualifier AS qualifier, " + - " NULL AS user_id, " + - " groups.id AS group_id " + - " FROM projects " + - " INNER JOIN group_roles ON group_roles.resource_id = projects.id AND group_roles.role = 'user' " + - " INNER JOIN groups ON groups.id = group_roles.group_id " + - " WHERE " + - " (projects.qualifier = 'TRK' " + - " or projects.qualifier = 'VW' " + - " or projects.qualifier = 'APP') " + - " AND projects.copy_component_uuid is NULL " + - " {projectsCondition} " + - " AND group_id IS NOT NULL " + - " UNION " + - - // public projects are accessible to any one - - " SELECT '" + RowKind.ANYONE + "' as kind," + - " projects.uuid AS project, " + - " projects.qualifier AS qualifier, " + - " NULL AS user_id, " + - " NULL AS group_id " + - " FROM projects " + - " WHERE " + - " (projects.qualifier = 'TRK' " + - " or projects.qualifier = 'VW' " + - " or projects.qualifier = 'APP') " + - " AND projects.copy_component_uuid is NULL " + - " AND projects.private = ? " + - " {projectsCondition} " + - " UNION " + - - // private project is returned when no authorization - " SELECT '" + RowKind.NONE + "' as kind," + - " projects.uuid AS project, " + - " projects.qualifier AS qualifier, " + - " NULL AS user_id, " + - " NULL AS group_id " + - " FROM projects " + - " WHERE " + - " (projects.qualifier = 'TRK' " + - " or projects.qualifier = 'VW' " + - " or projects.qualifier = 'APP') " + - " AND projects.copy_component_uuid is NULL " + - " AND projects.private = ? " + - " {projectsCondition} " + - - " ) project_authorization"; - - List selectAll(DbClient dbClient, DbSession session) { - return doSelectByProjects(dbClient, session, Collections.emptyList()); - } - - List selectByUuids(DbClient dbClient, DbSession session, Collection projectOrViewUuids) { - return executeLargeInputs(projectOrViewUuids, subProjectOrViewUuids -> doSelectByProjects(dbClient, session, subProjectOrViewUuids)); - } - - private static List doSelectByProjects(DbClient dbClient, DbSession session, List projectUuids) { - try { - Map dtosByProjectUuid = new HashMap<>(); - try (PreparedStatement stmt = createStatement(dbClient, session, projectUuids); - ResultSet rs = stmt.executeQuery()) { - while (rs.next()) { - processRow(rs, dtosByProjectUuid); - } - return ImmutableList.copyOf(dtosByProjectUuid.values()); - } - } catch (SQLException e) { - throw new IllegalStateException("Fail to select authorizations", e); - } - } - - private static PreparedStatement createStatement(DbClient dbClient, DbSession session, List projectUuids) throws SQLException { - String sql; - if (projectUuids.isEmpty()) { - sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", ""); - } else { - sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", " AND projects.uuid in (" + repeat("?", ", ", projectUuids.size()) + ")"); - } - PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql); - int index = 1; - // query for RowKind.USER - index = populateProjectUuidPlaceholders(stmt, projectUuids, index); - // query for RowKind.GROUP - index = populateProjectUuidPlaceholders(stmt, projectUuids, index); - // query for RowKind.ANYONE - index = setPrivateProjectPlaceHolder(stmt, index, false); - index = populateProjectUuidPlaceholders(stmt, projectUuids, index); - // query for RowKind.NONE - index = setPrivateProjectPlaceHolder(stmt, index, true); - populateProjectUuidPlaceholders(stmt, projectUuids, index); - return stmt; - } - - private static int populateProjectUuidPlaceholders(PreparedStatement stmt, List projectUuids, int index) throws SQLException { - int newIndex = index; - for (String projectUuid : projectUuids) { - stmt.setString(newIndex, projectUuid); - newIndex++; - } - return newIndex; - } - - private static int setPrivateProjectPlaceHolder(PreparedStatement stmt, int index, boolean isPrivate) throws SQLException { - int newIndex = index; - stmt.setBoolean(newIndex, isPrivate); - newIndex++; - return newIndex; - } - - private static void processRow(ResultSet rs, Map dtosByProjectUuid) throws SQLException { - RowKind rowKind = RowKind.valueOf(rs.getString(1)); - String projectUuid = rs.getString(2); - - Dto dto = dtosByProjectUuid.get(projectUuid); - if (dto == null) { - String qualifier = rs.getString(5); - dto = new Dto(projectUuid, qualifier); - dtosByProjectUuid.put(projectUuid, dto); - } - switch (rowKind) { - case NONE: - break; - case USER: - dto.addUserId(rs.getInt(3)); - break; - case GROUP: - dto.addGroupId(rs.getInt(4)); - break; - case ANYONE: - dto.allowAnyone(); - break; - } - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/user/BaseUserSession.java b/server/sonar-server-common/src/main/java/org/sonar/server/user/BaseUserSession.java deleted file mode 100644 index 51c591cd62f..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/user/BaseUserSession.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.user; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import org.sonar.core.permission.ProjectPermissions; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.permission.OrganizationPermission; - -import static org.apache.commons.lang.StringUtils.defaultString; - -public abstract class BaseUserSession implements UserSession { - @Override - public final boolean hasPermission(OrganizationPermission permission, OrganizationDto organization) { - return hasPermission(permission, organization.getUuid()); - } - - @Override - public final boolean hasPermission(OrganizationPermission permission, String organizationUuid) { - return isRoot() || hasPermissionImpl(permission, organizationUuid); - } - - protected abstract boolean hasPermissionImpl(OrganizationPermission permission, String organizationUuid); - - @Override - public final boolean hasComponentPermission(String permission, ComponentDto component) { - if (isRoot()) { - return true; - } - String projectUuid = defaultString(component.getMainBranchProjectUuid(), component.projectUuid()); - return hasProjectUuidPermission(permission, projectUuid); - } - - @Override - public final boolean hasComponentUuidPermission(String permission, String componentUuid) { - if (isRoot()) { - return true; - } - Optional projectUuid = componentUuidToProjectUuid(componentUuid); - return projectUuid - .map(s -> hasProjectUuidPermission(permission, s)) - .orElse(false); - } - - protected abstract Optional componentUuidToProjectUuid(String componentUuid); - - protected abstract boolean hasProjectUuidPermission(String permission, String projectUuid); - - @Override - public final boolean hasMembership(OrganizationDto organization) { - return isRoot() || hasMembershipImpl(organization); - } - - protected abstract boolean hasMembershipImpl(OrganizationDto organization); - - @Override - public final List keepAuthorizedComponents(String permission, Collection components) { - if (isRoot()) { - return new ArrayList<>(components); - } - return doKeepAuthorizedComponents(permission, components); - } - - /** - * Naive implementation, to be overridden if needed - */ - protected List doKeepAuthorizedComponents(String permission, Collection components) { - boolean allowPublicComponent = ProjectPermissions.PUBLIC_PERMISSIONS.contains(permission); - return components.stream() - .filter(c -> (allowPublicComponent && !c.isPrivate()) || hasComponentPermission(permission, c)) - .collect(MoreCollectors.toList()); - } -} diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/user/UserSession.java b/server/sonar-server-common/src/main/java/org/sonar/server/user/UserSession.java deleted file mode 100644 index 645254afcf3..00000000000 --- a/server/sonar-server-common/src/main/java/org/sonar/server/user/UserSession.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.user; - -import java.util.Collection; -import java.util.List; -import javax.annotation.CheckForNull; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.permission.OrganizationPermission; -import org.sonar.db.user.GroupDto; - -public interface UserSession { - - /** - * Login of the authenticated user. Returns {@code null} - * if {@link #isLoggedIn()} is {@code false}. - */ - @CheckForNull - String getLogin(); - - /** - * Uuid of the authenticated user. Returns {@code null} - * if {@link #isLoggedIn()} is {@code false}. - */ - @CheckForNull - String getUuid(); - - /** - * Name of the authenticated user. Returns {@code null} - * if {@link #isLoggedIn()} is {@code false}. - */ - @CheckForNull - String getName(); - - /** - * Database ID of the authenticated user. Returns {@code null} - * if {@link #isLoggedIn()} is {@code false}. - */ - @CheckForNull - Integer getUserId(); - - /** - * The groups that the logged-in user is member of. An empty - * collection is returned if {@link #isLoggedIn()} is {@code false}. - */ - Collection getGroups(); - - /** - * Whether the user is logged-in or anonymous. - */ - boolean isLoggedIn(); - - /** - * Whether the user has root privileges. If {@code true}, then user automatically - * benefits from all the permissions on all organizations and projects. - */ - boolean isRoot(); - - /** - * Ensures that {@link #isRoot()} returns {@code true} otherwise throws a - * {@link org.sonar.server.exceptions.ForbiddenException}. - */ - UserSession checkIsRoot(); - - /** - * Ensures that user is logged in otherwise throws {@link org.sonar.server.exceptions.UnauthorizedException}. - */ - UserSession checkLoggedIn(); - - /** - * Returns {@code true} if the permission is granted on the organization, otherwise {@code false}. - * - * If the organization does not exist, then returns {@code false}. - * - * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if - * organization does not exist. - */ - boolean hasPermission(OrganizationPermission permission, OrganizationDto organization); - - boolean hasPermission(OrganizationPermission permission, String organizationUuid); - - /** - * Ensures that {@link #hasPermission(OrganizationPermission, OrganizationDto)} is {@code true}, - * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. - */ - UserSession checkPermission(OrganizationPermission permission, OrganizationDto organization); - - UserSession checkPermission(OrganizationPermission permission, String organizationUuid); - - /** - * Returns {@code true} if the permission is granted to user on the component, - * otherwise {@code false}. - * - * If the component does not exist, then returns {@code false}. - * - * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if - * component does not exist. - * - * If the permission is not granted, then the organization permission is _not_ checked. - * - * @param component non-null component. - * @param permission project permission as defined by {@link org.sonar.core.permission.ProjectPermissions} - */ - boolean hasComponentPermission(String permission, ComponentDto component); - - /** - * Using {@link #hasComponentPermission(String, ComponentDto)} is recommended - * because it does not have to load project if the referenced component - * is not a project. - * - * @deprecated use {@link #hasComponentPermission(String, ComponentDto)} instead - */ - @Deprecated - boolean hasComponentUuidPermission(String permission, String componentUuid); - - /** - * Return the subset of specified components which the user has granted permission. - * An empty list is returned if input is empty or if no components are allowed to be - * accessed. - * If the input is ordered, then the returned components are in the same order. - * The duplicated components are returned duplicated too. - */ - List keepAuthorizedComponents(String permission, Collection components); - - /** - * Ensures that {@link #hasComponentPermission(String, ComponentDto)} is {@code true}, - * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. - */ - UserSession checkComponentPermission(String projectPermission, ComponentDto component); - - /** - * Ensures that {@link #hasComponentUuidPermission(String, String)} is {@code true}, - * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. - * - * @deprecated use {@link #checkComponentPermission(String, ComponentDto)} instead - */ - @Deprecated - UserSession checkComponentUuidPermission(String permission, String componentUuid); - - /** - * Whether user can administrate system, for example for using cross-organizations services - * like update center, system info or management of users. - * - * Returns {@code true} if: - *
    - *
  • {@link #isRoot()} is {@code true}
  • - *
  • organization feature is disabled and user is administrator of the (single) default organization
  • - *
- */ - boolean isSystemAdministrator(); - - /** - * Ensures that {@link #isSystemAdministrator()} is {@code true}, - * otherwise throws {@link org.sonar.server.exceptions.ForbiddenException}. - */ - UserSession checkIsSystemAdministrator(); - - /** - * Returns {@code true} if the user is member of the organization, otherwise {@code false}. - * - * If the organization does not exist, then returns {@code false}. - * - * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if - * organization does not exist. - */ - boolean hasMembership(OrganizationDto organization); - - /** - * Ensures that {@link #hasMembership(OrganizationDto)} is {@code true}, - * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. - */ - UserSession checkMembership(OrganizationDto organization); - -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java deleted file mode 100644 index e96e0e366aa..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.stream.IntStream; -import org.junit.Test; -import org.sonar.api.resources.Qualifiers; -import org.sonar.db.component.ComponentDto; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -public class ComponentIndexCombinationTest extends ComponentIndexTest { - - @Test - public void return_empty_list_if_no_fields_match_query() { - indexProject("struts", "Apache Struts"); - - assertThat(index.searchSuggestions(SuggestionQuery.builder().setQuery("missing").build()).isEmpty()).isTrue(); - } - - @Test - public void should_not_return_components_that_do_not_match_at_all() { - indexProject("banana", "Banana Project 1"); - - assertNoSearchResults("Apple"); - } - - @Test - public void filter_results_by_qualifier() { - ComponentDto project = indexProject("struts", "Apache Struts"); - indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); - - assertSearchResults(SuggestionQuery.builder().setQuery("struts").setQualifiers(singletonList(Qualifiers.PROJECT)).build(), project); - } - - @Test - public void should_limit_the_number_of_results() { - IntStream.rangeClosed(0, 10).forEach(i -> indexProject("sonarqube" + i, "SonarQube" + i)); - - assertSearch(SuggestionQuery.builder().setQuery("sonarqube").setLimit(5).setQualifiers(singletonList(Qualifiers.PROJECT)).build()).hasSize(5); - } - - @Test - public void should_not_support_wildcards() { - indexProject("theKey", "the name"); - - assertNoSearchResults("*t*"); - assertNoSearchResults("th?Key"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java deleted file mode 100644 index 7f781bff032..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.sonar.api.resources.Qualifiers.PROJECT; - -public class ComponentIndexFeatureExactTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(query -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.EXACT_IGNORE_CASE); - } - - @Test - public void scoring_cares_about_exact_matches() { - ComponentDto project1 = indexProject("project1", "LongNameLongNameLongNameLongNameSonarQube"); - ComponentDto project2 = indexProject("project2", "LongNameLongNameLongNameLongNameSonarQubeX"); - - SuggestionQuery query1 = SuggestionQuery.builder() - .setQuery("LongNameLongNameLongNameLongNameSonarQube") - .setQualifiers(Collections.singletonList(PROJECT)) - .build(); - assertSearch(query1).containsExactly(uuids(project1, project2)); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java deleted file mode 100644 index 7028889347f..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -import static com.google.common.collect.ImmutableSet.of; -import static java.util.Collections.singletonList; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.elasticsearch.index.query.QueryBuilders.termQuery; -import static org.sonar.api.resources.Qualifiers.PROJECT; -import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY; - -public class ComponentIndexFeatureFavoriteTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(q -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.FAVORITE); - } - - @Test - public void scoring_cares_about_favorites() { - ComponentDto project1 = indexProject("sonarqube", "SonarQube"); - ComponentDto project2 = indexProject("recent", "SonarQube Recently"); - - SuggestionQuery query1 = SuggestionQuery.builder() - .setQuery("SonarQube") - .setQualifiers(singletonList(PROJECT)) - .setFavoriteKeys(of(project1.getDbKey())) - .build(); - assertSearch(query1).containsExactly(uuids(project1, project2)); - - SuggestionQuery query2 = SuggestionQuery.builder() - .setQuery("SonarQube") - .setQualifiers(singletonList(PROJECT)) - .setFavoriteKeys(of(project2.getDbKey())) - .build(); - assertSearch(query2).containsExactly(uuids(project2, project1)); - } - - @Test - public void irrelevant_favorites_are_not_returned() { - features.set(q -> termQuery(FIELD_KEY, "non-existing-value"), ComponentTextSearchFeatureRepertoire.FAVORITE); - ComponentDto project1 = indexProject("foo", "foo"); - - SuggestionQuery query1 = SuggestionQuery.builder() - .setQuery("bar") - .setQualifiers(singletonList(PROJECT)) - .setFavoriteKeys(of(project1.getDbKey())) - .build(); - assertSearch(query1).isEmpty(); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java deleted file mode 100644 index 79c1ba832c4..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -public class ComponentIndexFeatureKeyTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(ComponentTextSearchFeatureRepertoire.KEY); - } - - @Test - public void should_search_projects_by_exact_case_insensitive_key() { - ComponentDto project1 = indexProject("keyOne", "Project One"); - indexProject("keyTwo", "Project Two"); - - assertSearchResults("keyOne", project1); - assertSearchResults("keyone", project1); - assertSearchResults("KEYone", project1); - } - - @Test - public void should_search_project_with_dot_in_key() { - ComponentDto project = indexProject("org.sonarqube", "SonarQube"); - - assertSearchResults("org.sonarqube", project); - assertNoSearchResults("orgsonarqube"); - } - - @Test - public void should_search_project_with_dash_in_key() { - ComponentDto project = indexProject("org-sonarqube", "SonarQube"); - - assertSearchResults("org-sonarqube", project); - assertNoSearchResults("orgsonarqube"); - } - - @Test - public void should_search_project_with_colon_in_key() { - ComponentDto project = indexProject("org:sonarqube", "Quality Product"); - - assertSearchResults("org:sonarqube", project); - assertNoSearchResults("orgsonarqube"); - assertNoSearchResults("org-sonarqube"); - assertNoSearchResults("org_sonarqube"); - } - - @Test - public void should_search_project_with_all_special_characters_in_key() { - ComponentDto project = indexProject("org.sonarqube:sonar-sérvèr_ç", "SonarQube"); - - assertSearchResults("org.sonarqube:sonar-sérvèr_ç", project); - } - - @Test - public void should_not_return_results_when_searching_by_partial_key() { - indexProject("theKey", "some name"); - - assertNoSearchResults("theke"); - assertNoSearchResults("hekey"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java deleted file mode 100644 index ee014835fc0..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -public class ComponentIndexFeaturePartialTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); - } - - @Test - public void search_projects_by_exact_name() { - ComponentDto struts = indexProject("struts", "Apache Struts"); - indexProject("sonarqube", "SonarQube"); - - assertSearchResults("Apache Struts", struts); - assertSearchResults("APACHE STRUTS", struts); - assertSearchResults("APACHE struTS", struts); - } - - @Test - public void search_file_with_long_name() { - ComponentDto project = indexProject("struts", "Apache Struts"); - ComponentDto file1 = indexFile(project, "src/main/java/DefaultRubyComponentServiceTestManagerFactory.java", "DefaultRubyComponentServiceTestManagerFactory.java"); - - assertSearchResults("DefaultRubyComponentServiceTestManagerFactory", file1); - assertSearchResults("DefaultRubyComponentServiceTestManagerFactory.java", file1); - assertSearchResults("RubyComponentServiceTestManager", file1); - assertSearchResults("te", file1); - } - - @Test - public void should_search_by_name_with_two_characters() { - ComponentDto project = indexProject("struts", "Apache Struts"); - - assertSearchResults("st", project); - assertSearchResults("tr", project); - } - - @Test - public void search_projects_by_partial_name() { - ComponentDto struts = indexProject("struts", "Apache Struts"); - - assertSearchResults("truts", struts); - assertSearchResults("pache", struts); - assertSearchResults("apach", struts); - assertSearchResults("che stru", struts); - } - - @Test - public void search_projects_and_files_by_partial_name() { - ComponentDto project = indexProject("struts", "Apache Struts"); - ComponentDto file1 = indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); - indexFile(project, "src/main/java/Foo.java", "Foo.java"); - - assertSearchResults("struts", project, file1); - assertSearchResults("Struts", project, file1); - assertSearchResults("StrutsManager", file1); - assertSearchResults("STRUTSMAN", file1); - assertSearchResults("utsManag", file1); - } - - @Test - public void should_find_file_by_file_extension() { - ComponentDto project = indexProject("struts", "Apache Struts"); - ComponentDto file1 = indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); - ComponentDto file2 = indexFile(project, "src/main/java/Foo.java", "Foo.java"); - - assertSearchResults(".java", file1, file2); - assertSearchResults("manager.java", file1); - - // do not match - assertNoSearchResults("somethingStrutsManager.java"); - } - - @Test - public void should_search_for_word_and_suffix() { - assertFileMatches("plugin java", "AbstractPluginFactory.java"); - } - - @Test - public void should_search_for_word_and_suffix_in_any_order() { - assertFileMatches("java plugin", "AbstractPluginFactory.java"); - } - - @Test - public void should_search_for_two_words() { - assertFileMatches("abstract factory", "AbstractPluginFactory.java"); - } - - @Test - public void should_search_for_two_words_in_any_order() { - assertFileMatches("factory abstract", "AbstractPluginFactory.java"); - } - - @Test - public void should_require_at_least_one_matching_word() { - assertNoFileMatches("monitor object", "AbstractPluginFactory.java"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java deleted file mode 100644 index e24735268f8..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Before; -import org.junit.Test; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -public class ComponentIndexFeaturePrefixTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(ComponentTextSearchFeatureRepertoire.PREFIX, ComponentTextSearchFeatureRepertoire.PREFIX_IGNORE_CASE); - } - - @Test - public void should_find_prefix() { - assertResultOrder("comp", "component"); - } - - @Test - public void should_find_exact_match() { - assertResultOrder("component.js", "component.js"); - } - - @Test - public void should_not_find_partially() { - assertNoFileMatches("component.js", "my_component.js"); - } - - @Test - public void should_be_able_to_ignore_case() { - features.set(ComponentTextSearchFeatureRepertoire.PREFIX_IGNORE_CASE); - assertResultOrder("cOmPoNeNt.Js", "CoMpOnEnT.jS"); - } - - @Test - public void should_prefer_matching_case() { - assertResultOrder("cOmPoNeNt.Js", "cOmPoNeNt.Js", "CoMpOnEnT.jS"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java deleted file mode 100644 index 54fa6595b83..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Collections; -import org.junit.Before; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -import static com.google.common.collect.ImmutableSet.of; -import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; -import static org.sonar.api.resources.Qualifiers.PROJECT; - -public class ComponentIndexFeatureRecentlyBrowsedTest extends ComponentIndexTest { - - @Before - public void before() { - features.set(query -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.RECENTLY_BROWSED); - } - - @Test - public void scoring_cares_about_recently_browsed() { - ComponentDto project1 = indexProject("sonarqube", "SonarQube"); - ComponentDto project2 = indexProject("recent", "SonarQube Recently"); - - SuggestionQuery query1 = SuggestionQuery.builder() - .setQuery("SonarQube") - .setQualifiers(Collections.singletonList(PROJECT)) - .setRecentlyBrowsedKeys(of(project1.getDbKey())) - .build(); - assertSearch(query1).containsExactly(uuids(project1, project2)); - - SuggestionQuery query2 = SuggestionQuery.builder() - .setQuery("SonarQube") - .setQualifiers(Collections.singletonList(PROJECT)) - .setRecentlyBrowsedKeys(of(project2.getDbKey())) - .build(); - assertSearch(query2).containsExactly(uuids(project2, project1)); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java deleted file mode 100644 index d0ff9409dbd..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Collections; -import java.util.Optional; -import java.util.stream.Stream; -import org.junit.Test; -import org.sonar.api.resources.Qualifiers; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ComponentIndexHighlightTest extends ComponentIndexTest { - - @Test - public void should_escape_html() { - assertHighlighting("quick< brown fox", "brown", "quick< brown fox"); - } - - @Test - public void should_highlight_partial_name() { - assertHighlighting("quickbrownfox", "brown", "quickbrownfox"); - } - - @Test - public void should_highlight_prefix() { - assertHighlighting("quickbrownfox", "quick", "quickbrownfox"); - } - - @Test - public void should_highlight_suffix() { - assertHighlighting("quickbrownfox", "fox", "quickbrownfox"); - } - - @Test - public void should_highlight_multiple_words() { - assertHighlighting("quickbrownfox", "fox bro", "quickbrownfox"); - } - - @Test - public void should_highlight_multiple_connected_words() { - assertHighlighting("quickbrownfox", "fox brown", "quickbrownfox"); - } - - private void assertHighlighting(String fileName, String search, String expectedHighlighting) { - indexFile(fileName); - - SuggestionQuery query = SuggestionQuery.builder() - .setQuery(search) - .setQualifiers(Collections.singletonList(Qualifiers.FILE)) - .build(); - Stream results = index.searchSuggestions(query, features.get()).getQualifiers(); - - assertThat(results).flatExtracting(ComponentHitsPerQualifier::getHits) - .extracting(ComponentHit::getHighlightedText) - .extracting(Optional::get) - .containsExactly(expectedHighlighting); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java deleted file mode 100644 index 24597b6dac4..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; - -import static org.sonar.db.user.GroupTesting.newGroupDto; -import static org.sonar.db.user.UserTesting.newUserDto; - -public class ComponentIndexLoginTest extends ComponentIndexTest { - - @Test - public void should_filter_unauthorized_results() { - indexer.index(newProject("sonarqube", "Quality Product")); - - // do not give any permissions to that project - - assertNoSearchResults("sonarqube"); - assertNoSearchResults("Quality Product"); - } - - @Test - public void should_find_project_for_which_the_user_has_direct_permission() { - UserDto user = newUserDto(); - userSession.logIn(user); - - ComponentDto project = newProject("sonarqube", "Quality Product"); - indexer.index(project); - - assertNoSearchResults("sonarqube"); - - // give the user explicit access - authorizationIndexerTester.allowOnlyUser(project, user); - assertSearchResults("sonarqube", project); - } - - @Test - public void should_find_project_for_which_the_user_has_indirect_permission_through_group() { - GroupDto group = newGroupDto(); - userSession.logIn().setGroups(group); - - ComponentDto project = newProject("sonarqube", "Quality Product"); - indexer.index(project); - - assertNoSearchResults("sonarqube"); - - // give the user implicit access (though group) - authorizationIndexerTester.allowOnlyGroup(project, group); - assertSearchResults("sonarqube", project); - } - - @Test - public void do_not_check_permissions_when_logged_in_user_is_root() { - userSession.logIn().setRoot(); - ComponentDto project = newProject("sonarqube", "Quality Product"); - indexer.index(project); - // do not give any permissions to that project - - assertSearchResults("sonarqube", project); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java deleted file mode 100644 index 6eda6e347c1..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import org.junit.Test; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -public class ComponentIndexMultipleWordsTest extends ComponentIndexTest { - - @Test - public void should_find_perfect_match() { - assertResultOrder("struts java", - "Struts.java"); - } - - @Test - public void should_find_partial_match() { - features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); - assertResultOrder("struts java", - "Xstrutsx.Xjavax"); - } - - @Test - public void should_find_partial_match_prefix_word1() { - assertResultOrder("struts java", - "MyStruts.java"); - } - - @Test - public void should_find_partial_match_suffix_word1() { - assertResultOrder("struts java", - "StrutsObject.java"); - } - - @Test - public void should_find_partial_match_prefix_word2() { - assertResultOrder("struts java", - "MyStruts.xjava"); - } - - @Test - public void should_find_partial_match_suffix_word2() { - assertResultOrder("struts java", - "MyStruts.javax"); - } - - @Test - public void should_find_partial_match_prefix_and_suffix_everywhere() { - assertResultOrder("struts java", - "MyStrutsObject.xjavax"); - } - - @Test - public void should_find_subset_of_document_terms() { - assertResultOrder("struts java", - "Some.Struts.Class.java.old"); - } - - @Test - public void should_require_all_words_to_match() { - assertNoFileMatches("struts java", - "Struts"); - } - - @Test - public void should_ignore_empty_words() { - assertFileMatches(" struts \n \n\n", - "Struts"); - } - - @Test - public void should_require_all_words_to_match_for_partial() { - features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); - assertNoFileMatches("struts java", - "Struts"); - } - -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java deleted file mode 100644 index de165d98273..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import com.google.common.collect.ImmutableSet; -import org.junit.Test; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; - -import static java.util.Arrays.asList; -import static org.sonar.api.resources.Qualifiers.FILE; -import static org.sonar.api.resources.Qualifiers.MODULE; -import static org.sonar.api.resources.Qualifiers.PROJECT; - -public class ComponentIndexScoreTest extends ComponentIndexTest { - - @Test - public void should_prefer_components_without_prefix() { - assertResultOrder("File.java", - "File.java", - "MyFile.java"); - } - - @Test - public void should_prefer_components_without_suffix() { - assertResultOrder("File", - "File", - "Filex"); - } - - @Test - public void should_prefer_key_matching_over_name_matching() { - ComponentDto project1 = indexProject("quality", "SonarQube"); - ComponentDto project2 = indexProject("sonarqube", "Quality Product"); - - assertExactResults("sonarqube", project2, project1); - } - - @Test - public void should_prefer_prefix_matching_over_partial_matching() { - assertResultOrder("corem", - "CoreMetrics.java", - "ScoreMatrix.java"); - } - - @Test - public void should_prefer_case_sensitive_prefix() { - assertResultOrder("caSe", - "caSeBla.java", - "CaseBla.java"); - } - - @Test - public void scoring_prefix_with_multiple_words() { - assertResultOrder("index java", - "IndexSomething.java", - "MyIndex.java"); - } - - @Test - public void scoring_prefix_with_multiple_words_and_case() { - assertResultOrder("Index JAVA", - "IndexSomething.java", - "index_java.js"); - } - - @Test - public void scoring_long_items() { - assertResultOrder("ThisIsAVeryLongNameToSearchForAndItExceeds15Characters.java", - "ThisIsAVeryLongNameToSearchForAndItExceeds15Characters.java", - "ThisIsAVeryLongNameToSearchForAndItEndsDifferently.java"); - } - - @Test - public void scoring_perfect_match() { - assertResultOrder("SonarQube", - "SonarQube", - "SonarQube SCM Git"); - } - - @Test - public void scoring_perfect_match_dispite_case_changes() { - assertResultOrder("sonarqube", - "SonarQube", - "SonarQube SCM Git"); - } - - @Test - public void scoring_perfect_match_with_matching_case_higher_than_without_matching_case() { - assertResultOrder("sonarqube", - "sonarqube", - "SonarQube"); - } - - @Test - public void should_prefer_favorite_over_recently_browsed() { - ComponentDto file1 = db.components().insertPrivateProject(c -> c.setName("File1")); - index(file1); - - ComponentDto file2 = db.components().insertPrivateProject(c -> c.setName("File2")); - index(file2); - - assertSearch(SuggestionQuery.builder() - .setQuery("File") - .setQualifiers(asList(PROJECT, MODULE, FILE)) - .setRecentlyBrowsedKeys(ImmutableSet.of(file1.getDbKey())) - .setFavoriteKeys(ImmutableSet.of(file2.getDbKey())) - .build()).containsExactly(uuids(file2, file1)); - - assertSearch(SuggestionQuery.builder() - .setQuery("File") - .setQualifiers(asList(PROJECT, MODULE, FILE)) - .setRecentlyBrowsedKeys(ImmutableSet.of(file2.getDbKey())) - .setFavoriteKeys(ImmutableSet.of(file1.getDbKey())) - .build()).containsExactly(uuids(file1, file2)); - } - - @Test - public void do_not_match_wrong_file_extension() { - ComponentDto file1 = indexFile("MyClass.java"); - ComponentDto file2 = indexFile("ClassExample.java"); - ComponentDto file3 = indexFile("Class.java"); - indexFile("Class.cs"); - indexFile("Class.js"); - indexFile("Class.rb"); - - assertExactResults("Class java", file3, file2, file1); - } - - @Test - public void if_relevancy_is_equal_fall_back_to_alphabetical_ordering() { - assertResultOrder("sonarqube", - "sonarqubeA", - "sonarqubeB"); - } - - @Test - public void scoring_test_DbTester() { - features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); - - ComponentDto project = indexProject("key-1", "Quality Product"); - - index(ComponentTesting.newFileDto(project) - .setName("DbTester.java") - .setDbKey("java/org/example/DbTester.java") - .setUuid("UUID-DbTester")); - - index(ComponentTesting.newFileDto(project) - .setName("WebhookDbTesting.java") - .setDbKey("java/org/example/WebhookDbTesting.java") - .setUuid("UUID-WebhookDbTesting")); - - assertSearch("dbt").containsExactly( - - "UUID-DbTester", - "UUID-WebhookDbTesting" - - ); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java deleted file mode 100644 index e22e83f147a..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.utils.System2; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.SearchIdResult; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRule; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Collections.emptySet; -import static java.util.Collections.singleton; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.db.component.ComponentTesting.newFileDto; - -public class ComponentIndexSearchTest { - @Rule - public EsTester es = EsTester.create(); - @Rule - public DbTester db = DbTester.create(System2.INSTANCE); - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - @Rule - public ComponentTextSearchFeatureRule features = new ComponentTextSearchFeatureRule(); - - private ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); - - private ComponentIndex underTest = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); - - @Test - public void filter_by_language() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto javaFile = db.components().insertComponent(newFileDto(project).setLanguage("java")); - ComponentDto jsFile1 = db.components().insertComponent(newFileDto(project).setLanguage("js")); - ComponentDto jsFile2 = db.components().insertComponent(newFileDto(project).setLanguage("js")); - index(project); - - SearchIdResult result = underTest.search(ComponentQuery.builder().setLanguage("js").build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(jsFile1.uuid(), jsFile2.uuid()); - } - - @Test - public void filter_by_name() { - ComponentDto ignoredProject = db.components().insertPrivateProject(p -> p.setName("ignored project")); - ComponentDto project = db.components().insertPrivateProject(p -> p.setName("Project Shiny name")); - index(ignoredProject, project); - - SearchIdResult result = underTest.search(ComponentQuery.builder().setQuery("shiny").build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); - } - - @Test - public void filter_by_key_with_exact_match() { - ComponentDto ignoredProject = db.components().insertPrivateProject(p -> p.setDbKey("ignored-project")); - ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("shiny-project")); - ComponentDto anotherIgnoreProject = db.components().insertPrivateProject(p -> p.setDbKey("another-shiny-project")); - index(ignoredProject, project); - - SearchIdResult result = underTest.search(ComponentQuery.builder().setQuery("shiny-project").build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); - } - - @Test - public void filter_by_qualifier() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project)); - index(project); - - SearchIdResult result = underTest.search(ComponentQuery.builder().setQualifiers(singleton(Qualifiers.FILE)).build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(file.uuid()); - } - - @Test - public void filter_by_organization() { - OrganizationDto organization = db.organizations().insert(); - OrganizationDto anotherOrganization = db.organizations().insert(); - ComponentDto project = db.components().insertPrivateProject(organization); - ComponentDto anotherProject = db.components().insertPrivateProject(anotherOrganization); - index(project, anotherProject); - - SearchIdResult result = underTest.search(ComponentQuery.builder().setOrganization(organization.getUuid()).build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); - } - - @Test - public void order_by_name_case_insensitive() { - ComponentDto project2 = db.components().insertPrivateProject(p -> p.setName("PROJECT 2")); - ComponentDto project3 = db.components().insertPrivateProject(p -> p.setName("project 3")); - ComponentDto project1 = db.components().insertPrivateProject(p -> p.setName("Project 1")); - index(project1, project2, project3); - - SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactly(project1.uuid(), project2.uuid(), project3.uuid()); - } - - @Test - public void paginate_results() { - List projects = IntStream.range(0, 9) - .mapToObj(i -> db.components().insertPrivateProject(p -> p.setName("project " + i))) - .collect(Collectors.toList()); - index(projects.toArray(new ComponentDto[0])); - - SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions().setPage(2, 3)); - - assertThat(result.getIds()).containsExactlyInAnyOrder(projects.get(3).uuid(), projects.get(4).uuid(), projects.get(5).uuid()); - } - - @Test - public void filter_unauthorized_components() { - ComponentDto unauthorizedProject = db.components().insertPrivateProject(); - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto project2 = db.components().insertPrivateProject(); - indexer.indexOnStartup(emptySet()); - authorizationIndexerTester.allowOnlyAnyone(project1); - authorizationIndexerTester.allowOnlyAnyone(project2); - - SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions()); - - assertThat(result.getIds()).containsExactlyInAnyOrder(project1.uuid(), project2.uuid()) - .doesNotContain(unauthorizedProject.uuid()); - } - - private void index(ComponentDto... components) { - indexer.indexOnStartup(emptySet()); - Arrays.stream(components).forEach(c -> authorizationIndexerTester.allowOnlyAnyone(c)); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java deleted file mode 100644 index 9079c9c11c8..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.component.index; - -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; -import org.assertj.core.api.ListAssert; -import org.junit.Before; -import org.junit.Rule; -import org.sonar.api.utils.System2; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.organization.OrganizationTesting; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRule; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.api.resources.Qualifiers.FILE; -import static org.sonar.api.resources.Qualifiers.MODULE; -import static org.sonar.api.resources.Qualifiers.PROJECT; - -public abstract class ComponentIndexTest { - - @Rule - public EsTester es = EsTester.create(); - @Rule - public DbTester db = DbTester.create(System2.INSTANCE); - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - - @Rule - public ComponentTextSearchFeatureRule features = new ComponentTextSearchFeatureRule(); - - protected ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); - protected ComponentIndex index = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); - protected PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); - private OrganizationDto organization; - - @Before - public void setUp() { - organization = OrganizationTesting.newOrganizationDto(); - } - - protected void assertFileMatches(String query, String... fileNames) { - ComponentDto[] files = Arrays.stream(fileNames) - .map(this::indexFile) - .toArray(ComponentDto[]::new); - assertSearch(query).containsExactlyInAnyOrder(uuids(files)); - } - - protected void assertNoFileMatches(String query, String... fileNames) { - Arrays.stream(fileNames) - .forEach(this::indexFile); - assertSearch(query).isEmpty(); - } - - protected void assertResultOrder(String query, String... resultsInOrder) { - ComponentDto project = indexProject("key-1", "Quality Product"); - List files = Arrays.stream(resultsInOrder) - .map(r -> ComponentTesting.newFileDto(project).setName(r)) - .peek(f -> f.setUuid(f.uuid() + "_" + f.name().replaceAll("[^a-zA-Z0-9]", ""))) - .collect(Collectors.toList()); - - // index them, but not in the expected order - files.stream() - .sorted(Comparator.comparing(ComponentDto::uuid).reversed()) - .forEach(this::index); - - assertExactResults(query, files.toArray(new ComponentDto[0])); - } - - protected ListAssert assertSearch(String query) { - return assertSearch(SuggestionQuery.builder().setQuery(query).setQualifiers(asList(PROJECT, MODULE, FILE)).build()); - } - - protected ListAssert assertSearch(SuggestionQuery query) { - return (ListAssert)assertThat(index.searchSuggestions(query, features.get()).getQualifiers()) - .flatExtracting(ComponentHitsPerQualifier::getHits) - .extracting(ComponentHit::getUuid); - } - - protected void assertSearchResults(String query, ComponentDto... expectedComponents) { - assertSearchResults(SuggestionQuery.builder().setQuery(query).setQualifiers(asList(PROJECT, MODULE, FILE)).build(), expectedComponents); - } - - protected void assertSearchResults(SuggestionQuery query, ComponentDto... expectedComponents) { - assertSearch(query).containsOnly(uuids(expectedComponents)); - } - - protected void assertExactResults(String query, ComponentDto... expectedComponents) { - assertSearch(query).containsExactly(uuids(expectedComponents)); - } - - protected void assertNoSearchResults(String query) { - assertSearchResults(query); - } - - protected ComponentDto indexProject(String key, String name) { - return index( - ComponentTesting.newPrivateProjectDto(organization, "UUID_" + key) - .setDbKey(key) - .setName(name)); - } - - protected ComponentDto newProject(String key, String name) { - return ComponentTesting.newPrivateProjectDto(organization, "UUID_" + key) - .setDbKey(key) - .setName(name); - } - - protected ComponentDto indexFile(String fileName) { - ComponentDto project = indexProject("key-1", "SonarQube"); - return indexFile(project, "src/main/java/" + fileName, fileName); - } - - protected ComponentDto indexFile(ComponentDto project, String fileKey, String fileName) { - return index( - ComponentTesting.newFileDto(project) - .setDbKey(fileKey) - .setName(fileName)); - } - - protected ComponentDto index(ComponentDto dto) { - indexer.index(dto); - authorizationIndexerTester.allowOnlyAnyone(dto); - return dto; - } - - protected static String[] uuids(ComponentDto... expectedComponents) { - return Arrays.stream(expectedComponents).map(ComponentDto::uuid).toArray(String[]::new); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/es/EsModuleTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/es/EsModuleTest.java index 972f8ad3ee9..ed1931efa9c 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/es/EsModuleTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/es/EsModuleTest.java @@ -23,13 +23,14 @@ import org.junit.Test; import org.sonar.core.platform.ComponentContainer; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; public class EsModuleTest { @Test public void verify_count_of_added_components() { ComponentContainer container = new ComponentContainer(); new EsModule().configure(container); - assertThat(container.size()).isEqualTo(3 + 2); + assertThat(container.size()).isEqualTo(2 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER); } } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryFactoryTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryFactoryTest.java deleted file mode 100644 index b5894ff4e20..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryFactoryTest.java +++ /dev/null @@ -1,548 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue; - -import java.time.Clock; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.rule.RuleKey; -import org.sonar.api.utils.DateUtils; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.SnapshotDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDbTester; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.sonar.api.utils.DateUtils.addDays; -import static org.sonar.db.component.ComponentTesting.newDirectory; -import static org.sonar.db.component.ComponentTesting.newFileDto; -import static org.sonar.db.component.ComponentTesting.newModuleDto; -import static org.sonar.db.component.ComponentTesting.newProjectCopy; -import static org.sonar.db.component.ComponentTesting.newSubView; -import static org.sonar.db.rule.RuleTesting.newRule; - -public class IssueQueryFactoryTest { - - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Rule - public DbTester db = DbTester.create(); - - private RuleDbTester ruleDbTester = new RuleDbTester(db); - - private Clock clock = mock(Clock.class); - private IssueQueryFactory underTest = new IssueQueryFactory(db.getDbClient(), clock, userSession); - - @Test - public void create_from_parameters() { - UserDto user = db.users().insertUser(u -> u.setLogin("joanna")); - OrganizationDto organization = db.organizations().insert(); - ComponentDto project = db.components().insertPrivateProject(organization); - ComponentDto module = db.components().insertComponent(newModuleDto(project)); - ComponentDto file = db.components().insertComponent(newFileDto(project)); - - RuleDefinitionDto rule1 = ruleDbTester.insert(); - RuleDefinitionDto rule2 = ruleDbTester.insert(); - newRule(RuleKey.of("findbugs", "NullReference")); - SearchRequest request = new SearchRequest() - .setIssues(asList("anIssueKey")) - .setSeverities(asList("MAJOR", "MINOR")) - .setStatuses(asList("CLOSED")) - .setResolutions(asList("FALSE-POSITIVE")) - .setResolved(true) - .setProjectKeys(asList(project.getDbKey())) - .setModuleUuids(asList(module.uuid())) - .setDirectories(asList("aDirPath")) - .setFileUuids(asList(file.uuid())) - .setAssigneesUuid(asList(user.getUuid())) - .setLanguages(asList("xoo")) - .setTags(asList("tag1", "tag2")) - .setOrganization(organization.getKey()) - .setAssigned(true) - .setCreatedAfter("2013-04-16T09:08:24+0200") - .setCreatedBefore("2013-04-17T09:08:24+0200") - .setRules(asList(rule1.getKey().toString(), rule2.getKey().toString())) - .setSort("CREATION_DATE") - .setAsc(true); - - IssueQuery query = underTest.create(request); - - assertThat(query.issueKeys()).containsOnly("anIssueKey"); - assertThat(query.severities()).containsOnly("MAJOR", "MINOR"); - assertThat(query.statuses()).containsOnly("CLOSED"); - assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE"); - assertThat(query.resolved()).isTrue(); - assertThat(query.projectUuids()).containsOnly(project.uuid()); - assertThat(query.moduleUuids()).containsOnly(module.uuid()); - assertThat(query.fileUuids()).containsOnly(file.uuid()); - assertThat(query.assignees()).containsOnly(user.getUuid()); - assertThat(query.languages()).containsOnly("xoo"); - assertThat(query.tags()).containsOnly("tag1", "tag2"); - assertThat(query.organizationUuid()).isEqualTo(organization.getUuid()); - assertThat(query.onComponentOnly()).isFalse(); - assertThat(query.assigned()).isTrue(); - assertThat(query.rules()).hasSize(2); - assertThat(query.directories()).containsOnly("aDirPath"); - assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200")); - assertThat(query.createdAfter().inclusive()).isTrue(); - assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDateTime("2013-04-17T09:08:24+0200")); - assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE); - assertThat(query.asc()).isTrue(); - } - - @Test - public void leak_period_start_date_is_exclusive() { - long leakPeriodStart = addDays(new Date(), -14).getTime(); - - ComponentDto project = db.components().insertPublicProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project)); - - SnapshotDto analysis = db.components().insertSnapshot(project, s -> s.setPeriodDate(leakPeriodStart)); - - SearchRequest request = new SearchRequest() - .setComponentUuids(Collections.singletonList(file.uuid())) - .setOnComponentOnly(true) - .setSinceLeakPeriod(true); - - IssueQuery query = underTest.create(request); - - assertThat(query.componentUuids()).containsOnly(file.uuid()); - assertThat(query.createdAfter().date()).isEqualTo(new Date(leakPeriodStart)); - assertThat(query.createdAfter().inclusive()).isFalse(); - - } - - @Test - public void dates_are_inclusive() { - SearchRequest request = new SearchRequest() - .setCreatedAfter("2013-04-16") - .setCreatedBefore("2013-04-17"); - - IssueQuery query = underTest.create(request); - - assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDate("2013-04-16")); - assertThat(query.createdAfter().inclusive()).isTrue(); - assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDate("2013-04-18")); - } - - @Test - public void creation_date_support_localdate() { - SearchRequest request = new SearchRequest() - .setCreatedAt("2013-04-16"); - - IssueQuery query = underTest.create(request); - - assertThat(query.createdAt()).isEqualTo(DateUtils.parseDate("2013-04-16")); - } - - @Test - public void creation_date_support_zoneddatetime() { - SearchRequest request = new SearchRequest() - .setCreatedAt("2013-04-16T09:08:24+0200"); - - IssueQuery query = underTest.create(request); - - assertThat(query.createdAt()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200")); - } - - @Test - public void add_unknown_when_no_component_found() { - SearchRequest request = new SearchRequest() - .setComponentKeys(asList("does_not_exist")); - - IssueQuery query = underTest.create(request); - - assertThat(query.componentUuids()).containsOnly(""); - } - - @Test - public void query_without_any_parameter() { - SearchRequest request = new SearchRequest(); - - IssueQuery query = underTest.create(request); - - assertThat(query.componentUuids()).isEmpty(); - assertThat(query.projectUuids()).isEmpty(); - assertThat(query.moduleUuids()).isEmpty(); - assertThat(query.moduleRootUuids()).isEmpty(); - assertThat(query.directories()).isEmpty(); - assertThat(query.fileUuids()).isEmpty(); - assertThat(query.viewUuids()).isEmpty(); - assertThat(query.organizationUuid()).isNull(); - assertThat(query.branchUuid()).isNull(); - } - - @Test - public void fail_if_components_and_components_uuid_params_are_set_at_the_same_time() { - SearchRequest request = new SearchRequest() - .setComponentKeys(asList("foo")) - .setComponentUuids(asList("bar")); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("At most one of the following parameters can be provided: componentKeys, componentUuids, components, componentRoots, componentUuids"); - - underTest.create(request); - } - - @Test - public void fail_if_both_projects_and_projectUuids_params_are_set() { - SearchRequest request = new SearchRequest() - .setProjectKeys(asList("foo")) - .setProjectUuids(asList("bar")); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Parameters projects and projectUuids cannot be set simultaneously"); - - underTest.create(request); - } - - @Test - public void fail_if_both_componentRoots_and_componentRootUuids_params_are_set() { - SearchRequest request = new SearchRequest() - .setComponentRoots(asList("foo")) - .setComponentRootUuids(asList("bar")); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("At most one of the following parameters can be provided: componentKeys, componentUuids, components, componentRoots, componentUuids"); - - underTest.create(request); - } - - @Test - public void fail_if_componentRoots_references_components_with_different_qualifier() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project)); - SearchRequest request = new SearchRequest() - .setComponentRoots(asList(project.getDbKey(), file.getDbKey())); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("All components must have the same qualifier, found FIL,TRK"); - - underTest.create(request); - } - - @Test - public void param_componentRootUuids_enables_search_in_view_tree_if_user_has_permission_on_view() { - ComponentDto view = db.components().insertView(); - SearchRequest request = new SearchRequest() - .setComponentRootUuids(asList(view.uuid())); - userSession.registerComponents(view); - - IssueQuery query = underTest.create(request); - - assertThat(query.viewUuids()).containsOnly(view.uuid()); - assertThat(query.onComponentOnly()).isFalse(); - } - - @Test - public void application_search_project_issues() { - ComponentDto project1 = db.components().insertPublicProject(); - ComponentDto project2 = db.components().insertPublicProject(); - ComponentDto application = db.components().insertApplication(db.getDefaultOrganization()); - db.components().insertComponents(newProjectCopy("PC1", project1, application)); - db.components().insertComponents(newProjectCopy("PC2", project2, application)); - userSession.registerComponents(application, project1, project2); - - IssueQuery result = underTest.create(new SearchRequest().setComponentUuids(singletonList(application.uuid()))); - - assertThat(result.viewUuids()).containsExactlyInAnyOrder(application.uuid()); - } - - @Test - public void application_search_project_issues_on_leak() { - Date now = new Date(); - ComponentDto project1 = db.components().insertPublicProject(); - SnapshotDto analysis1 = db.components().insertSnapshot(project1, s -> s.setPeriodDate(addDays(now, -14).getTime())); - ComponentDto project2 = db.components().insertPublicProject(); - SnapshotDto analysis2 = db.components().insertSnapshot(project2, s -> s.setPeriodDate(null)); - ComponentDto project3 = db.components().insertPublicProject(); - ComponentDto application = db.components().insertApplication(db.getDefaultOrganization()); - db.components().insertComponents(newProjectCopy("PC1", project1, application)); - db.components().insertComponents(newProjectCopy("PC2", project2, application)); - db.components().insertComponents(newProjectCopy("PC3", project3, application)); - userSession.registerComponents(application, project1, project2, project3); - - IssueQuery result = underTest.create(new SearchRequest() - .setComponentUuids(singletonList(application.uuid())) - .setSinceLeakPeriod(true)); - - assertThat(result.createdAfterByProjectUuids()).hasSize(1); - assertThat(result.createdAfterByProjectUuids().get(project1.uuid()).date().getTime()).isEqualTo(analysis1.getPeriodDate()); - assertThat(result.createdAfterByProjectUuids().get(project1.uuid()).inclusive()).isFalse(); - assertThat(result.viewUuids()).containsExactlyInAnyOrder(application.uuid()); - } - - @Test - public void return_empty_results_if_not_allowed_to_search_for_subview() { - ComponentDto view = db.components().insertView(); - ComponentDto subView = db.components().insertComponent(newSubView(view)); - SearchRequest request = new SearchRequest() - .setComponentRootUuids(asList(subView.uuid())); - - IssueQuery query = underTest.create(request); - - assertThat(query.viewUuids()).containsOnly(""); - } - - @Test - public void param_componentUuids_enables_search_on_project_tree_by_default() { - ComponentDto project = db.components().insertPrivateProject(); - SearchRequest request = new SearchRequest() - .setComponentUuids(asList(project.uuid())); - - IssueQuery query = underTest.create(request); - assertThat(query.projectUuids()).containsExactly(project.uuid()); - assertThat(query.onComponentOnly()).isFalse(); - } - - @Test - public void onComponentOnly_restricts_search_to_specified_componentKeys() { - ComponentDto project = db.components().insertPrivateProject(); - SearchRequest request = new SearchRequest() - .setComponentKeys(asList(project.getDbKey())) - .setOnComponentOnly(true); - - IssueQuery query = underTest.create(request); - - assertThat(query.projectUuids()).isEmpty(); - assertThat(query.componentUuids()).containsExactly(project.uuid()); - assertThat(query.onComponentOnly()).isTrue(); - } - - @Test - public void should_search_in_tree_with_module_uuid() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto module = db.components().insertComponent(newModuleDto(project)); - SearchRequest request = new SearchRequest() - .setComponentUuids(asList(module.uuid())); - - IssueQuery query = underTest.create(request); - assertThat(query.moduleRootUuids()).containsExactly(module.uuid()); - assertThat(query.onComponentOnly()).isFalse(); - } - - @Test - public void param_componentUuids_enables_search_in_directory_tree() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto dir = db.components().insertComponent(newDirectory(project, "src/main/java/foo")); - SearchRequest request = new SearchRequest() - .setComponentUuids(asList(dir.uuid())); - - IssueQuery query = underTest.create(request); - - assertThat(query.moduleUuids()).containsOnly(dir.moduleUuid()); - assertThat(query.directories()).containsOnly(dir.path()); - assertThat(query.onComponentOnly()).isFalse(); - } - - @Test - public void param_componentUuids_enables_search_by_file() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project)); - SearchRequest request = new SearchRequest() - .setComponentUuids(asList(file.uuid())); - - IssueQuery query = underTest.create(request); - - assertThat(query.fileUuids()).containsExactly(file.uuid()); - } - - @Test - public void param_componentUuids_enables_search_by_test_file() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project).setQualifier(Qualifiers.UNIT_TEST_FILE)); - SearchRequest request = new SearchRequest() - .setComponentUuids(asList(file.uuid())); - - IssueQuery query = underTest.create(request); - - assertThat(query.fileUuids()).containsExactly(file.uuid()); - } - - @Test - public void search_issue_from_branch() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project); - - assertThat(underTest.create(new SearchRequest() - .setProjectKeys(singletonList(branch.getKey())) - .setBranch(branch.getBranch()))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(project.uuid()), false); - - assertThat(underTest.create(new SearchRequest() - .setComponentKeys(singletonList(branch.getKey())) - .setBranch(branch.getBranch()))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(project.uuid()), false); - } - - @Test - public void search_file_issue_from_branch() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project); - ComponentDto file = db.components().insertComponent(newFileDto(branch)); - - assertThat(underTest.create(new SearchRequest() - .setComponentKeys(singletonList(file.getKey())) - .setBranch(branch.getBranch()))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(file.uuid()), false); - - assertThat(underTest.create(new SearchRequest() - .setComponentKeys(singletonList(branch.getKey())) - .setFileUuids(singletonList(file.uuid())) - .setBranch(branch.getBranch()))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(file.uuid()), false); - - assertThat(underTest.create(new SearchRequest() - .setProjectKeys(singletonList(branch.getKey())) - .setFileUuids(singletonList(file.uuid())) - .setBranch(branch.getBranch()))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(file.uuid()), false); - } - - @Test - public void search_issue_on_component_only_from_branch() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project); - ComponentDto file = db.components().insertComponent(newFileDto(branch)); - - assertThat(underTest.create(new SearchRequest() - .setComponentKeys(singletonList(file.getKey())) - .setBranch(branch.getBranch()) - .setOnComponentOnly(true))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.componentUuids()), IssueQuery::isMainBranch) - .containsOnly(branch.uuid(), singletonList(file.uuid()), false); - } - - @Test - public void search_issues_from_main_branch() { - ComponentDto project = db.components().insertMainBranch(); - ComponentDto branch = db.components().insertProjectBranch(project); - - assertThat(underTest.create(new SearchRequest() - .setProjectKeys(singletonList(project.getKey())) - .setBranch("master"))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) - .containsOnly(project.uuid(), singletonList(project.uuid()), true); - assertThat(underTest.create(new SearchRequest() - .setComponentKeys(singletonList(project.getKey())) - .setBranch("master"))) - .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) - .containsOnly(project.uuid(), singletonList(project.uuid()), true); - } - - @Test - public void fail_if_created_after_and_created_since_are_both_set() { - SearchRequest request = new SearchRequest() - .setCreatedAfter("2013-07-25T07:35:00+0100") - .setCreatedInLast("palap"); - - try { - underTest.create(request); - fail(); - } catch (Exception e) { - assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Parameters createdAfter and createdInLast cannot be set simultaneously"); - } - } - - @Test - public void set_created_after_from_created_since() { - Date now = DateUtils.parseDateTime("2013-07-25T07:35:00+0100"); - when(clock.instant()).thenReturn(now.toInstant()); - when(clock.getZone()).thenReturn(ZoneOffset.UTC); - SearchRequest request = new SearchRequest() - .setCreatedInLast("1y2m3w4d"); - assertThat(underTest.create(request).createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2012-04-30T07:35:00+0100")); - assertThat(underTest.create(request).createdAfter().inclusive()).isTrue(); - - } - - @Test - public void fail_if_since_leak_period_and_created_after_set_at_the_same_time() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Parameters 'createdAfter' and 'sinceLeakPeriod' cannot be set simultaneously"); - - underTest.create(new SearchRequest() - .setSinceLeakPeriod(true) - .setCreatedAfter("2013-07-25T07:35:00+0100")); - } - - @Test - public void fail_if_no_component_provided_with_since_leak_period() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("One and only one component must be provided when searching since leak period"); - - underTest.create(new SearchRequest().setSinceLeakPeriod(true)); - } - - @Test - public void fail_if_several_components_provided_with_since_leak_period() { - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto project2 = db.components().insertPrivateProject(); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("One and only one component must be provided when searching since leak period"); - - underTest.create(new SearchRequest() - .setSinceLeakPeriod(true) - .setComponentKeys(asList(project1.getKey(), project2.getKey()))); - } - - @Test - public void fail_if_date_is_not_formatted_correctly() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("'unknown-date' cannot be parsed as either a date or date+time"); - - underTest.create(new SearchRequest() - .setCreatedAfter("unknown-date")); - } - - @Test - public void return_empty_results_if_organization_with_specified_key_does_not_exist() { - SearchRequest request = new SearchRequest() - .setOrganization("does_not_exist"); - - IssueQuery query = underTest.create(request); - - assertThat(query.organizationUuid()).isEqualTo(""); - } - -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryTest.java deleted file mode 100644 index 957de3d743f..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueQueryTest.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import java.util.Date; -import org.junit.Test; -import org.sonar.api.issue.Issue; -import org.sonar.api.rule.Severity; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.server.issue.IssueQuery.PeriodStart; - -import static com.google.common.collect.Lists.newArrayList; -import static org.apache.commons.lang.math.RandomUtils.nextInt; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; - -public class IssueQueryTest { - - @Test - public void build_query() { - RuleDefinitionDto rule = new RuleDefinitionDto().setId(nextInt(1000)); - PeriodStart filterDate = new IssueQuery.PeriodStart(new Date(10_000_000_000L), false); - IssueQuery query = IssueQuery.builder() - .issueKeys(newArrayList("ABCDE")) - .severities(newArrayList(Severity.BLOCKER)) - .statuses(Lists.newArrayList(Issue.STATUS_RESOLVED)) - .resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE)) - .projectUuids(newArrayList("PROJECT")) - .componentUuids(newArrayList("org/struts/Action.java")) - .moduleUuids(newArrayList("org.struts:core")) - .rules(newArrayList(rule)) - .assigneeUuids(newArrayList("gargantua")) - .languages(newArrayList("xoo")) - .tags(newArrayList("tag1", "tag2")) - .types(newArrayList("RELIABILITY", "SECURITY")) - .owaspTop10(newArrayList("a1", "a2")) - .sansTop25(newArrayList("insecure-interaction", "porous-defenses")) - .cwe(newArrayList("12", "125")) - .organizationUuid("orga") - .branchUuid("my_branch") - .createdAfterByProjectUuids(ImmutableMap.of("PROJECT", filterDate)) - .assigned(true) - .createdAfter(new Date()) - .createdBefore(new Date()) - .createdAt(new Date()) - .resolved(true) - .sort(IssueQuery.SORT_BY_CREATION_DATE) - .asc(true) - .build(); - assertThat(query.issueKeys()).containsOnly("ABCDE"); - assertThat(query.severities()).containsOnly(Severity.BLOCKER); - assertThat(query.statuses()).containsOnly(Issue.STATUS_RESOLVED); - assertThat(query.resolutions()).containsOnly(Issue.RESOLUTION_FALSE_POSITIVE); - assertThat(query.projectUuids()).containsOnly("PROJECT"); - assertThat(query.componentUuids()).containsOnly("org/struts/Action.java"); - assertThat(query.moduleUuids()).containsOnly("org.struts:core"); - assertThat(query.assignees()).containsOnly("gargantua"); - assertThat(query.languages()).containsOnly("xoo"); - assertThat(query.tags()).containsOnly("tag1", "tag2"); - assertThat(query.types()).containsOnly("RELIABILITY", "SECURITY"); - assertThat(query.owaspTop10()).containsOnly("a1", "a2"); - assertThat(query.sansTop25()).containsOnly("insecure-interaction", "porous-defenses"); - assertThat(query.cwe()).containsOnly("12", "125"); - assertThat(query.organizationUuid()).isEqualTo("orga"); - assertThat(query.branchUuid()).isEqualTo("my_branch"); - assertThat(query.createdAfterByProjectUuids()).containsOnly(entry("PROJECT", filterDate)); - assertThat(query.assigned()).isTrue(); - assertThat(query.rules()).containsOnly(rule); - assertThat(query.createdAfter()).isNotNull(); - assertThat(query.createdBefore()).isNotNull(); - assertThat(query.createdAt()).isNotNull(); - assertThat(query.resolved()).isTrue(); - assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE); - assertThat(query.asc()).isTrue(); - } - - @Test - public void build_query_without_dates() { - IssueQuery query = IssueQuery.builder() - .issueKeys(newArrayList("ABCDE")) - .createdAfter(null) - .createdBefore(null) - .createdAt(null) - .build(); - - assertThat(query.issueKeys()).containsOnly("ABCDE"); - assertThat(query.createdAfter()).isNull(); - assertThat(query.createdBefore()).isNull(); - assertThat(query.createdAt()).isNull(); - } - - @Test - public void throw_exception_if_sort_is_not_valid() { - try { - IssueQuery.builder() - .sort("UNKNOWN") - .build(); - } catch (Exception e) { - assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Bad sort field: UNKNOWN"); - } - } - - @Test - public void collection_params_should_not_be_null_but_empty() { - IssueQuery query = IssueQuery.builder() - .issueKeys(null) - .projectUuids(null) - .componentUuids(null) - .moduleUuids(null) - .statuses(null) - .assigneeUuids(null) - .resolutions(null) - .rules(null) - .severities(null) - .languages(null) - .tags(null) - .types(null) - .owaspTop10(null) - .sansTop25(null) - .cwe(null) - .createdAfterByProjectUuids(null) - .build(); - assertThat(query.issueKeys()).isEmpty(); - assertThat(query.projectUuids()).isEmpty(); - assertThat(query.componentUuids()).isEmpty(); - assertThat(query.moduleUuids()).isEmpty(); - assertThat(query.statuses()).isEmpty(); - assertThat(query.assignees()).isEmpty(); - assertThat(query.resolutions()).isEmpty(); - assertThat(query.rules()).isEmpty(); - assertThat(query.severities()).isEmpty(); - assertThat(query.languages()).isEmpty(); - assertThat(query.tags()).isEmpty(); - assertThat(query.types()).isEmpty(); - assertThat(query.owaspTop10()).isEmpty(); - assertThat(query.sansTop25()).isEmpty(); - assertThat(query.cwe()).isEmpty(); - assertThat(query.createdAfterByProjectUuids()).isEmpty(); - } - - @Test - public void test_default_query() { - IssueQuery query = IssueQuery.builder().build(); - assertThat(query.issueKeys()).isEmpty(); - assertThat(query.projectUuids()).isEmpty(); - assertThat(query.componentUuids()).isEmpty(); - assertThat(query.moduleUuids()).isEmpty(); - assertThat(query.statuses()).isEmpty(); - assertThat(query.assignees()).isEmpty(); - assertThat(query.resolutions()).isEmpty(); - assertThat(query.rules()).isEmpty(); - assertThat(query.severities()).isEmpty(); - assertThat(query.languages()).isEmpty(); - assertThat(query.tags()).isEmpty(); - assertThat(query.types()).isEmpty(); - assertThat(query.organizationUuid()).isNull(); - assertThat(query.branchUuid()).isNull(); - assertThat(query.assigned()).isNull(); - assertThat(query.createdAfter()).isNull(); - assertThat(query.createdBefore()).isNull(); - assertThat(query.resolved()).isNull(); - assertThat(query.sort()).isNull(); - assertThat(query.createdAfterByProjectUuids()).isEmpty(); - } - - @Test - public void should_accept_null_sort() { - IssueQuery query = IssueQuery.builder().sort(null).build(); - assertThat(query.sort()).isNull(); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java deleted file mode 100644 index b7dac4da87d..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue.index; - -import java.util.Map; -import java.util.TimeZone; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.issue.Issue; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.rule.RuleKey; -import org.sonar.api.rule.Severity; -import org.sonar.api.utils.System2; -import org.sonar.api.utils.internal.TestSystem2; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.Facets; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.issue.IssueDocTesting; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQuery.Builder; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Arrays.asList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.sonar.api.issue.Issue.STATUS_CLOSED; -import static org.sonar.api.issue.Issue.STATUS_OPEN; -import static org.sonar.api.utils.DateUtils.parseDateTime; -import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT; -import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; - -public class IssueIndexDebtTest { - - private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(TimeZone.getTimeZone("GMT-01:00")); - - @Rule - public EsTester es = EsTester.create(); - @Rule - public LightUserSessionRule userSessionRule = new LightUserSessionRule(); - @Rule - public DbTester db = DbTester.create(system2); - - private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); - private IssueIndex underTest; - - @Before - public void setUp() { - underTest = new IssueIndex(es.client(), system2, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); - } - - @Test - public void facets_on_projects() { - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto, "ABCD"); - ComponentDto project2 = ComponentTesting.newPrivateProjectDto(organizationDto, "EFGH"); - - indexIssues( - IssueDocTesting.newDoc("I1", ComponentTesting.newFileDto(project, null)).setEffort(10L), - IssueDocTesting.newDoc("I2", ComponentTesting.newFileDto(project, null)).setEffort(10L), - IssueDocTesting.newDoc("I3", ComponentTesting.newFileDto(project2, null)).setEffort(10L)); - - Facets facets = search("projectUuids"); - assertThat(facets.getNames()).containsOnly("projectUuids", FACET_MODE_EFFORT); - assertThat(facets.get("projectUuids")).containsOnly(entry("ABCD", 20L), entry("EFGH", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); - } - - @Test - public void facets_on_components() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto(), "A"); - ComponentDto file1 = ComponentTesting.newFileDto(project, null, "ABCD"); - ComponentDto file2 = ComponentTesting.newFileDto(project, null, "BCDE"); - ComponentDto file3 = ComponentTesting.newFileDto(project, null, "CDEF"); - - indexIssues( - IssueDocTesting.newDoc("I1", project).setEffort(10L), - IssueDocTesting.newDoc("I2", file1).setEffort(10L), - IssueDocTesting.newDoc("I3", file2).setEffort(10L), - IssueDocTesting.newDoc("I4", file2).setEffort(10L), - IssueDocTesting.newDoc("I5", file3).setEffort(10L)); - - Facets facets = search("fileUuids"); - assertThat(facets.getNames()).containsOnly("fileUuids", FACET_MODE_EFFORT); - assertThat(facets.get("fileUuids")) - .containsOnly(entry("A", 10L), entry("ABCD", 10L), entry("BCDE", 20L), entry("CDEF", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 50L)); - } - - @Test - public void facets_on_directories() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file1 = ComponentTesting.newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); - ComponentDto file2 = ComponentTesting.newFileDto(project, null).setPath("F2.xoo"); - - indexIssues( - IssueDocTesting.newDoc("I1", file1).setDirectoryPath("/src/main/xoo").setEffort(10L), - IssueDocTesting.newDoc("I2", file2).setDirectoryPath("/").setEffort(10L)); - - Facets facets = search("directories"); - assertThat(facets.getNames()).containsOnly("directories", FACET_MODE_EFFORT); - assertThat(facets.get("directories")).containsOnly(entry("/src/main/xoo", 10L), entry("/", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 20L)); - } - - @Test - public void facets_on_severities() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - indexIssues( - IssueDocTesting.newDoc("I1", file).setSeverity(Severity.INFO).setEffort(10L), - IssueDocTesting.newDoc("I2", file).setSeverity(Severity.INFO).setEffort(10L), - IssueDocTesting.newDoc("I3", file).setSeverity(Severity.MAJOR).setEffort(10L)); - - Facets facets = search("severities"); - assertThat(facets.getNames()).containsOnly("severities", FACET_MODE_EFFORT); - assertThat(facets.get("severities")).containsOnly(entry("INFO", 20L), entry("MAJOR", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); - } - - @Test - public void facets_on_statuses() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - indexIssues( - IssueDocTesting.newDoc("I1", file).setStatus(STATUS_CLOSED).setEffort(10L), - IssueDocTesting.newDoc("I2", file).setStatus(STATUS_CLOSED).setEffort(10L), - IssueDocTesting.newDoc("I3", file).setStatus(STATUS_OPEN).setEffort(10L)); - - Facets facets = search("statuses"); - assertThat(facets.getNames()).containsOnly("statuses", FACET_MODE_EFFORT); - assertThat(facets.get("statuses")).containsOnly(entry("CLOSED", 20L), entry("OPEN", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); - } - - @Test - public void facets_on_resolutions() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - indexIssues( - IssueDocTesting.newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setEffort(10L), - IssueDocTesting.newDoc("I2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setEffort(10L), - IssueDocTesting.newDoc("I3", file).setResolution(Issue.RESOLUTION_FIXED).setEffort(10L)); - - Facets facets = search("resolutions"); - assertThat(facets.getNames()).containsOnly("resolutions", FACET_MODE_EFFORT); - assertThat(facets.get("resolutions")).containsOnly(entry("FALSE-POSITIVE", 20L), entry("FIXED", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); - } - - @Test - public void facets_on_languages() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - RuleKey ruleKey = RuleKey.of("repo", "X1"); - - indexIssues(IssueDocTesting.newDoc("I1", file).setLanguage("xoo").setEffort(10L)); - - Facets facets = search("languages"); - assertThat(facets.getNames()).containsOnly("languages", FACET_MODE_EFFORT); - assertThat(facets.get("languages")).containsOnly(entry("xoo", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 10L)); - } - - private Facets search(String additionalFacet) { - return new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList(additionalFacet))), system2.getDefaultTimeZone()); - } - - @Test - public void facets_on_assignees() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - indexIssues( - IssueDocTesting.newDoc("I1", file).setAssigneeUuid("uuid-steph").setEffort(10L), - IssueDocTesting.newDoc("I2", file).setAssigneeUuid("uuid-simon").setEffort(10L), - IssueDocTesting.newDoc("I3", file).setAssigneeUuid("uuid-simon").setEffort(10L), - IssueDocTesting.newDoc("I4", file).setAssigneeUuid(null).setEffort(10L)); - - Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("assignees"))), system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly("assignees", FACET_MODE_EFFORT); - assertThat(facets.get("assignees")).containsOnly(entry("uuid-steph", 10L), entry("uuid-simon", 20L), entry("", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L)); - } - - @Test - public void facets_on_authors() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - indexIssues( - IssueDocTesting.newDoc("I1", file).setAuthorLogin("steph").setEffort(10L), - IssueDocTesting.newDoc("I2", file).setAuthorLogin("simon").setEffort(10L), - IssueDocTesting.newDoc("I3", file).setAuthorLogin("simon").setEffort(10L), - IssueDocTesting.newDoc("I4", file).setAuthorLogin(null).setEffort(10L)); - - Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("authors"))), system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly("authors", FACET_MODE_EFFORT); - assertThat(facets.get("authors")).containsOnly(entry("steph", 10L), entry("simon", 20L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L)); - } - - @Test - public void facet_on_created_at() { - SearchOptions searchOptions = fixtureForCreatedAtFacet(); - - Builder query = newQueryBuilder().createdBefore(parseDateTime("2016-01-01T00:00:00+0100")); - Map createdAt = new Facets(underTest.search(query.build(), searchOptions), system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2011-01-01", 10L), - entry("2012-01-01", 0L), - entry("2013-01-01", 0L), - entry("2014-01-01", 50L), - entry("2015-01-01", 10L)); - } - - @Test - public void deprecated_debt_facets() { - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto, "ABCD"); - ComponentDto project2 = ComponentTesting.newPrivateProjectDto(organizationDto, "EFGH"); - - indexIssues( - IssueDocTesting.newDoc("I1", ComponentTesting.newFileDto(project, null)).setEffort(10L), - IssueDocTesting.newDoc("I2", ComponentTesting.newFileDto(project, null)).setEffort(10L), - IssueDocTesting.newDoc("I3", ComponentTesting.newFileDto(project2, null)).setEffort(10L)); - - Facets facets = new Facets(underTest.search(IssueQuery.builder().facetMode(DEPRECATED_FACET_MODE_DEBT).build(), - new SearchOptions().addFacets(asList("projectUuids"))), system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly("projectUuids", FACET_MODE_EFFORT); - assertThat(facets.get("projectUuids")).containsOnly(entry("ABCD", 20L), entry("EFGH", 10L)); - assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); - } - - private SearchOptions fixtureForCreatedAtFacet() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = ComponentTesting.newFileDto(project, null); - - IssueDoc issue0 = IssueDocTesting.newDoc("ISSUE0", file).setEffort(10L).setFuncCreationDate(parseDateTime("2011-04-25T01:05:13+0100")); - IssueDoc issue1 = IssueDocTesting.newDoc("I1", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-01T12:34:56+0100")); - IssueDoc issue2 = IssueDocTesting.newDoc("I2", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-01T23:46:00+0100")); - IssueDoc issue3 = IssueDocTesting.newDoc("I3", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-02T12:34:56+0100")); - IssueDoc issue4 = IssueDocTesting.newDoc("I4", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-05T12:34:56+0100")); - IssueDoc issue5 = IssueDocTesting.newDoc("I5", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-20T12:34:56+0100")); - IssueDoc issue6 = IssueDocTesting.newDoc("I6", file).setEffort(10L).setFuncCreationDate(parseDateTime("2015-01-18T12:34:56+0100")); - - indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6); - - return new SearchOptions().addFacets("createdAt"); - } - - private void indexIssues(IssueDoc... issues) { - issueIndexer.index(asList(issues).iterator()); - for (IssueDoc issue : issues) { - addIssueAuthorization(issue.projectUuid()); - } - } - - private void addIssueAuthorization(String projectUuid) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(projectUuid, Qualifiers.PROJECT); - access.allowAnyone(); - authorizationIndexerTester.allow(access); - } - - private Builder newQueryBuilder() { - return IssueQuery.builder().facetMode(FACET_MODE_EFFORT); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java deleted file mode 100644 index eda244b09a7..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue.index; - -import java.util.Date; -import java.util.List; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.issue.Issue; -import org.sonar.api.utils.System2; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.tuple; -import static org.mockito.Mockito.mock; -import static org.sonar.db.component.ComponentTesting.newBranchDto; -import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; -import static org.sonar.db.component.ComponentTesting.newProjectBranch; -import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; -import static org.sonar.server.issue.IssueDocTesting.newDoc; - -public class IssueIndexProjectStatisticsTest { - - private System2 system2 = mock(System2.class); - @Rule - public EsTester es = EsTester.create(); - @Rule - public LightUserSessionRule userSessionRule = new LightUserSessionRule(); - - private IssueIndexer issueIndexer = new IssueIndexer(es.client(), null, new IssueIteratorFactory(null)); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); - - private IssueIndex underTest = new IssueIndex(es.client(), system2, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); - - @Test - public void searchProjectStatistics_returns_empty_list_if_no_input() { - List result = underTest.searchProjectStatistics(emptyList(), emptyList(), "unknownUser"); - assertThat(result).isEmpty(); - } - - @Test - public void searchProjectStatistics_returns_empty_list_if_the_input_does_not_match_anything() { - List result = underTest.searchProjectStatistics(singletonList("unknownProjectUuid"), singletonList(1_111_234_567_890L), "unknownUser"); - assertThat(result).isEmpty(); - } - - @Test - public void searchProjectStatistics_returns_something() { - OrganizationDto organization = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(organization); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); - } - - @Test - public void searchProjectStatistics_does_not_return_results_if_assignee_does_not_match() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String user1Uuid = randomAlphanumeric(40); - String user2Uuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(user1Uuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), user2Uuid); - - assertThat(result).isEmpty(); - } - - @Test - public void searchProjectStatistics_returns_results_if_assignee_matches() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String user1Uuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(user1Uuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), user1Uuid); - - assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); - } - - @Test - public void searchProjectStatistics_returns_results_if_functional_date_is_strictly_after_from_date() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); - } - - @Test - public void searchProjectStatistics_does_not_return_results_if_functional_date_is_same_as_from_date() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); - } - - @Test - public void searchProjectStatistics_does_not_return_resolved_issues() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues( - newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), - newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_FIXED), - newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_REMOVED), - newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_WONT_FIX)); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).isEmpty(); - } - - @Test - public void searchProjectStatistics_does_not_return_results_if_functional_date_is_before_from_date() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from - 1000L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).isEmpty(); - } - - @Test - public void searchProjectStatistics_returns_issue_count() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues( - newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue2", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue3", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result).extracting(ProjectStatistics::getIssueCount).containsExactly(3L); - } - - @Test - public void searchProjectStatistics_returns_issue_count_for_multiple_projects() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(org1); - ComponentDto project2 = newPrivateProjectDto(org1); - ComponentDto project3 = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues( - newDoc("issue1", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue2", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue3", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - - newDoc("issue4", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue5", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics( - asList(project1.uuid(), project2.uuid(), project3.uuid()), - asList(from, from, from), - userUuid); - - assertThat(result) - .extracting(ProjectStatistics::getProjectUuid, ProjectStatistics::getIssueCount) - .containsExactlyInAnyOrder( - tuple(project1.uuid(), 3L), - tuple(project3.uuid(), 2L)); - } - - @Test - public void searchProjectStatistics_returns_max_date_for_multiple_projects() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(org1); - ComponentDto project2 = newPrivateProjectDto(org1); - ComponentDto project3 = newPrivateProjectDto(org1); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_000L; - indexIssues( - newDoc("issue1", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1_000L)), - newDoc("issue2", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 2_000L)), - newDoc("issue3", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 3_000L)), - - newDoc("issue4", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 4_000L)), - newDoc("issue5", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 5_000L))); - - List result = underTest.searchProjectStatistics( - asList(project1.uuid(), project2.uuid(), project3.uuid()), - asList(from, from, from), - userUuid); - - assertThat(result) - .extracting(ProjectStatistics::getProjectUuid, ProjectStatistics::getLastIssueDate) - .containsExactlyInAnyOrder( - tuple(project1.uuid(), from + 3_000L), - tuple(project3.uuid(), from + 5_000L)); - } - - @Test - public void searchProjectStatistics_return_branch_issues() { - OrganizationDto organization = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(organization); - ComponentDto branch = newProjectBranch(project, newBranchDto(project).setKey("branch")); - String userUuid = randomAlphanumeric(40); - long from = 1_111_234_567_890L; - indexIssues( - newDoc("issue1", branch).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), - newDoc("issue2", branch).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 2L)), - newDoc("issue3", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); - - List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); - - assertThat(result) - .extracting(ProjectStatistics::getIssueCount, ProjectStatistics::getProjectUuid, ProjectStatistics::getLastIssueDate) - .containsExactly( - tuple(2L, branch.uuid(), from + 2L), - tuple(1L, project.uuid(), from + 1L)); - } - - private void indexIssues(IssueDoc... issues) { - issueIndexer.index(asList(issues).iterator()); - for (IssueDoc issue : issues) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(issue.projectUuid(), "TRK"); - access.allowAnyone(); - authorizationIndexerTester.allow(access); - } - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java deleted file mode 100644 index eddde5f4622..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java +++ /dev/null @@ -1,1753 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.issue.index; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.google.common.collect.Iterators; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.OptionalInt; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import org.assertj.core.api.Fail; -import org.assertj.core.groups.Tuple; -import org.elasticsearch.action.search.SearchResponse; -import org.elasticsearch.search.SearchHit; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.issue.Issue; -import org.sonar.api.rule.Severity; -import org.sonar.api.rules.RuleType; -import org.sonar.api.utils.System2; -import org.sonar.api.utils.internal.TestSystem2; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDefinitionDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.Facets; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.rule.index.RuleIndexer; -import org.sonar.server.user.LightUserSessionRule; -import org.sonar.server.view.index.ViewDoc; -import org.sonar.server.view.index.ViewIndexer; - -import static com.google.common.collect.ImmutableSortedSet.of; -import static java.util.Arrays.asList; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; -import static java.util.TimeZone.getTimeZone; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.assertj.core.api.Assertions.tuple; -import static org.junit.rules.ExpectedException.none; -import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; -import static org.sonar.api.resources.Qualifiers.APP; -import static org.sonar.api.rules.RuleType.BUG; -import static org.sonar.api.rules.RuleType.CODE_SMELL; -import static org.sonar.api.rules.RuleType.VULNERABILITY; -import static org.sonar.api.utils.DateUtils.addDays; -import static org.sonar.api.utils.DateUtils.parseDate; -import static org.sonar.api.utils.DateUtils.parseDateTime; -import static org.sonar.db.component.ComponentTesting.newFileDto; -import static org.sonar.db.component.ComponentTesting.newModuleDto; -import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; -import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; -import static org.sonar.db.rule.RuleTesting.newRule; -import static org.sonar.db.user.GroupTesting.newGroupDto; -import static org.sonar.db.user.UserTesting.newUserDto; -import static org.sonar.server.issue.IssueDocTesting.newDoc; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; - -public class IssueIndexTest { - - @Rule - public EsTester es = EsTester.create(); - @Rule - public LightUserSessionRule userSessionRule = new LightUserSessionRule(); - @Rule - public ExpectedException expectedException = none(); - private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(getTimeZone("GMT-01:00")); - @Rule - public DbTester db = DbTester.create(system2); - - private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private ViewIndexer viewIndexer = new ViewIndexer(db.getDbClient(), es.client()); - private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient()); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); - - private IssueIndex underTest = new IssueIndex(es.client(), system2, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); - - @Test - public void filter_by_keys() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - - indexIssues( - newDoc("I1", newFileDto(project, null)), - newDoc("I2", newFileDto(project, null))); - - assertThatSearchReturnsOnly(IssueQuery.builder().issueKeys(asList("I1", "I2")), "I1", "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().issueKeys(singletonList("I1")), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().issueKeys(asList("I3", "I4"))); - } - - @Test - public void filter_by_projects() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto module = newModuleDto(project); - ComponentDto subModule = newModuleDto(module); - - indexIssues( - newDoc("I1", project), - newDoc("I2", newFileDto(project, null)), - newDoc("I3", module), - newDoc("I4", newFileDto(module, null)), - newDoc("I5", subModule), - newDoc("I6", newFileDto(subModule, null))); - - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); - } - - @Test - public void facet_on_projectUuids() { - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(organizationDto, "ABCD"); - ComponentDto project2 = newPrivateProjectDto(organizationDto, "EFGH"); - - indexIssues( - newDoc("I1", newFileDto(project, null)), - newDoc("I2", newFileDto(project, null)), - newDoc("I3", newFileDto(project2, null))); - - assertThatFacetHasExactly(IssueQuery.builder(), "projectUuids", entry("ABCD", 2L), entry("EFGH", 1L)); - } - - @Test - public void filter_by_modules() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto module = newModuleDto(project); - ComponentDto subModule = newModuleDto(module); - ComponentDto file = newFileDto(subModule, null); - - indexIssues( - newDoc("I3", module), - newDoc("I5", subModule), - newDoc("I2", file)); - - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(file.uuid()))); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(module.uuid())), "I3"); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(subModule.uuid())), "I2", "I5"); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(project.uuid()))); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList("unknown"))); - } - - @Test - public void filter_by_components_on_contextualized_search() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto module = newModuleDto(project); - ComponentDto subModule = newModuleDto(module); - ComponentDto file1 = newFileDto(project, null); - ComponentDto file2 = newFileDto(module, null); - ComponentDto file3 = newFileDto(subModule, null); - String view = "ABCD"; - indexView(view, asList(project.uuid())); - - indexIssues( - newDoc("I1", project), - newDoc("I2", file1), - newDoc("I3", module), - newDoc("I4", file2), - newDoc("I5", subModule), - newDoc("I6", file3)); - - assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(asList(file1.uuid(), file2.uuid(), file3.uuid())), "I2", "I4", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(file1.uuid())), "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().moduleRootUuids(singletonList(subModule.uuid())), "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().moduleRootUuids(singletonList(module.uuid())), "I3", "I4", "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view)), "I1", "I2", "I3", "I4", "I5", "I6"); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); - } - - @Test - public void filter_by_components_on_non_contextualized_search() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto(), "project"); - ComponentDto file1 = newFileDto(project, null, "file1"); - ComponentDto module = newModuleDto(project).setUuid("module"); - ComponentDto file2 = newFileDto(module, null, "file2"); - ComponentDto subModule = newModuleDto(module).setUuid("subModule"); - ComponentDto file3 = newFileDto(subModule, null, "file3"); - String view = "ABCD"; - indexView(view, asList(project.uuid())); - - indexIssues( - newDoc("I1", project), - newDoc("I2", file1), - newDoc("I3", module), - newDoc("I4", file2), - newDoc("I5", subModule), - newDoc("I6", file3)); - - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view)), "I1", "I2", "I3", "I4", "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(module.uuid())), "I3", "I4"); - assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(subModule.uuid())), "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(file1.uuid())), "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(asList(file1.uuid(), file2.uuid(), file3.uuid())), "I2", "I4", "I6"); - } - - @Test - public void facets_on_components() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto(), "A"); - ComponentDto file1 = newFileDto(project, null, "ABCD"); - ComponentDto file2 = newFileDto(project, null, "BCDE"); - ComponentDto file3 = newFileDto(project, null, "CDEF"); - - indexIssues( - newDoc("I1", project), - newDoc("I2", file1), - newDoc("I3", file2), - newDoc("I4", file2), - newDoc("I5", file3)); - - assertThatFacetHasOnly(IssueQuery.builder(), "fileUuids", entry("A", 1L), entry("ABCD", 1L), entry("BCDE", 2L), entry("CDEF", 1L)); - } - - @Test - public void filter_by_directories() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file1 = newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); - ComponentDto file2 = newFileDto(project, null).setPath("F2.xoo"); - - indexIssues( - newDoc("I1", file1).setDirectoryPath("/src/main/xoo"), - newDoc("I2", file2).setDirectoryPath("/")); - - assertThatSearchReturnsOnly(IssueQuery.builder().directories(singletonList("/src/main/xoo")), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().directories(singletonList("/")), "I2"); - assertThatSearchReturnsEmpty(IssueQuery.builder().directories(singletonList("unknown"))); - } - - @Test - public void facets_on_directories() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file1 = newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); - ComponentDto file2 = newFileDto(project, null).setPath("F2.xoo"); - - indexIssues( - newDoc("I1", file1).setDirectoryPath("/src/main/xoo"), - newDoc("I2", file2).setDirectoryPath("/")); - - assertThatFacetHasOnly(IssueQuery.builder(), "directories", entry("/src/main/xoo", 1L), entry("/", 1L)); - } - - @Test - public void filter_by_portfolios() { - ComponentDto portfolio1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto portfolio2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project1)); - ComponentDto project2 = db.components().insertPrivateProject(); - - IssueDoc issueOnProject1 = newDoc(project1); - IssueDoc issueOnFile = newDoc(file); - IssueDoc issueOnProject2 = newDoc(project2); - - indexIssues(issueOnProject1, issueOnFile, issueOnProject2); - indexView(portfolio1.uuid(), singletonList(project1.uuid())); - indexView(portfolio2.uuid(), singletonList(project2.uuid())); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())), issueOnProject1.key(), issueOnFile.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio2.uuid())), issueOnProject2.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(asList(portfolio1.uuid(), portfolio2.uuid())), issueOnProject1.key(), issueOnFile.key(), issueOnProject2.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())).projectUuids(singletonList(project1.uuid())), issueOnProject1.key(), - issueOnFile.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())).fileUuids(singletonList(file.uuid())), issueOnFile.key()); - assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList("unknown"))); - } - - @Test - public void filter_by_portfolios_not_having_projects() { - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(organizationDto); - ComponentDto file1 = newFileDto(project1, null); - indexIssues(newDoc("I2", file1)); - String view1 = "ABCD"; - indexView(view1, emptyList()); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view1))); - } - - @Test - public void do_not_return_issues_from_project_branch_when_filtering_by_portfolios() { - ComponentDto portfolio = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto project = db.components().insertMainBranch(); - ComponentDto projectBranch = db.components().insertProjectBranch(project); - ComponentDto fileOnProjectBranch = db.components().insertComponent(newFileDto(projectBranch)); - indexView(portfolio.uuid(), singletonList(project.uuid())); - - IssueDoc issueOnProject = newDoc(project); - IssueDoc issueOnProjectBranch = newDoc(projectBranch); - IssueDoc issueOnFileOnProjectBranch = newDoc(fileOnProjectBranch); - indexIssues(issueOnProject, issueOnFileOnProjectBranch, issueOnProjectBranch); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())), issueOnProject.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())).projectUuids(singletonList(project.uuid())), - issueOnProject.key()); - assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())).projectUuids(singletonList(projectBranch.uuid()))); - } - - @Test - public void filter_one_issue_by_project_and_branch() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project); - ComponentDto anotherbBranch = db.components().insertProjectBranch(project); - - IssueDoc issueOnProject = newDoc(project); - IssueDoc issueOnBranch = newDoc(branch); - IssueDoc issueOnAnotherBranch = newDoc(anotherbBranch); - indexIssues(issueOnProject, issueOnBranch, issueOnAnotherBranch); - - assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().componentUuids(singletonList(branch.uuid())).branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); - assertThatSearchReturnsOnly( - IssueQuery.builder().componentUuids(singletonList(branch.uuid())).projectUuids(singletonList(project.uuid())).branchUuid(branch.uuid()).mainBranch(false), - issueOnBranch.key()); - assertThatSearchReturnsEmpty(IssueQuery.builder().branchUuid("unknown")); - } - - @Test - public void issues_from_branch_component_children() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto projectModule = db.components().insertComponent(newModuleDto(project)); - ComponentDto projectFile = db.components().insertComponent(newFileDto(projectModule)); - ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); - ComponentDto branchModule = db.components().insertComponent(newModuleDto(branch)); - ComponentDto branchFile = db.components().insertComponent(newFileDto(branchModule)); - - indexIssues( - newDoc("I1", project), - newDoc("I2", projectFile), - newDoc("I3", projectModule), - newDoc("I4", branch), - newDoc("I5", branchModule), - newDoc("I6", branchFile)); - - assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(branch.uuid()).mainBranch(false), "I4", "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(branchModule.uuid())).branchUuid(branch.uuid()).mainBranch(false), "I5", "I6"); - assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(branchFile.uuid())).branchUuid(branch.uuid()).mainBranch(false), "I6"); - assertThatSearchReturnsEmpty(IssueQuery.builder().fileUuids(singletonList(branchFile.uuid())).mainBranch(false).branchUuid("unknown")); - } - - @Test - public void issues_from_main_branch() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project); - - IssueDoc issueOnProject = newDoc(project); - IssueDoc issueOnBranch = newDoc(branch); - indexIssues(issueOnProject, issueOnBranch); - - assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().componentUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); - assertThatSearchReturnsOnly( - IssueQuery.builder().componentUuids(singletonList(project.uuid())).projectUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), - issueOnProject.key()); - } - - @Test - public void branch_issues_are_ignored_when_no_branch_param() { - ComponentDto project = db.components().insertPrivateProject(); - ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); - - IssueDoc projectIssue = newDoc(project); - IssueDoc branchIssue = newDoc(branch); - indexIssues(projectIssue, branchIssue); - - assertThatSearchReturnsOnly(IssueQuery.builder(), projectIssue.key()); - } - - @Test - public void filter_by_main_application() { - ComponentDto application1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto application2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project1)); - ComponentDto project2 = db.components().insertPrivateProject(); - indexView(application1.uuid(), singletonList(project1.uuid())); - indexView(application2.uuid(), singletonList(project2.uuid())); - - IssueDoc issueOnProject1 = newDoc(project1); - IssueDoc issueOnFile = newDoc(file); - IssueDoc issueOnProject2 = newDoc(project2); - indexIssues(issueOnProject1, issueOnFile, issueOnProject2); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())), issueOnProject1.key(), issueOnFile.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application2.uuid())), issueOnProject2.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(asList(application1.uuid(), application2.uuid())), issueOnProject1.key(), issueOnFile.key(), issueOnProject2.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())).projectUuids(singletonList(project1.uuid())), issueOnProject1.key(), - issueOnFile.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())).fileUuids(singletonList(file.uuid())), issueOnFile.key()); - assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList("unknown"))); - } - - @Test - public void filter_by_application_branch() { - ComponentDto application = db.components().insertMainBranch(c -> c.setQualifier(APP)); - ComponentDto branch1 = db.components().insertProjectBranch(application); - ComponentDto branch2 = db.components().insertProjectBranch(application); - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto file = db.components().insertComponent(newFileDto(project1)); - ComponentDto project2 = db.components().insertPrivateProject(); - indexView(branch1.uuid(), singletonList(project1.uuid())); - indexView(branch2.uuid(), singletonList(project2.uuid())); - - IssueDoc issueOnProject1 = newDoc(project1); - IssueDoc issueOnFile = newDoc(file); - IssueDoc issueOnProject2 = newDoc(project2); - indexIssues(issueOnProject1, issueOnFile, issueOnProject2); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).branchUuid(branch1.uuid()).mainBranch(false), - issueOnProject1.key(), issueOnFile.key()); - assertThatSearchReturnsOnly( - IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).projectUuids(singletonList(project1.uuid())).branchUuid(branch1.uuid()).mainBranch(false), - issueOnProject1.key(), issueOnFile.key()); - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).fileUuids(singletonList(file.uuid())).branchUuid(branch1.uuid()).mainBranch(false), - issueOnFile.key()); - assertThatSearchReturnsEmpty(IssueQuery.builder().branchUuid("unknown")); - } - - @Test - public void filter_by_application_branch_having_project_branches() { - ComponentDto application = db.components().insertMainBranch(c -> c.setQualifier(APP).setDbKey("app")); - ComponentDto applicationBranch1 = db.components().insertProjectBranch(application, a -> a.setKey("app-branch1")); - ComponentDto applicationBranch2 = db.components().insertProjectBranch(application, a -> a.setKey("app-branch2")); - ComponentDto project1 = db.components().insertPrivateProject(p -> p.setDbKey("prj1")); - ComponentDto project1Branch1 = db.components().insertProjectBranch(project1); - ComponentDto fileOnProject1Branch1 = db.components().insertComponent(newFileDto(project1Branch1)); - ComponentDto project1Branch2 = db.components().insertProjectBranch(project1); - ComponentDto project2 = db.components().insertPrivateProject(p -> p.setDbKey("prj2")); - indexView(applicationBranch1.uuid(), asList(project1Branch1.uuid(), project2.uuid())); - indexView(applicationBranch2.uuid(), singletonList(project1Branch2.uuid())); - - IssueDoc issueOnProject1 = newDoc(project1); - IssueDoc issueOnProject1Branch1 = newDoc(project1Branch1); - IssueDoc issueOnFileOnProject1Branch1 = newDoc(fileOnProject1Branch1); - IssueDoc issueOnProject1Branch2 = newDoc(project1Branch2); - IssueDoc issueOnProject2 = newDoc(project2); - indexIssues(issueOnProject1, issueOnProject1Branch1, issueOnFileOnProject1Branch1, issueOnProject1Branch2, issueOnProject2); - - assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).branchUuid(applicationBranch1.uuid()).mainBranch(false), - issueOnProject1Branch1.key(), issueOnFileOnProject1Branch1.key(), issueOnProject2.key()); - assertThatSearchReturnsOnly( - IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).projectUuids(singletonList(project1.uuid())).branchUuid(applicationBranch1.uuid()).mainBranch(false), - issueOnProject1Branch1.key(), issueOnFileOnProject1Branch1.key()); - assertThatSearchReturnsOnly( - IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).fileUuids(singletonList(fileOnProject1Branch1.uuid())).branchUuid(applicationBranch1.uuid()) - .mainBranch(false), - issueOnFileOnProject1Branch1.key()); - assertThatSearchReturnsEmpty( - IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).projectUuids(singletonList("unknown")).branchUuid(applicationBranch1.uuid()).mainBranch(false)); - } - - @Test - public void filter_by_created_after_by_projects() { - Date now = new Date(); - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(organizationDto); - IssueDoc project1Issue1 = newDoc(project1).setFuncCreationDate(addDays(now, -10)); - IssueDoc project1Issue2 = newDoc(project1).setFuncCreationDate(addDays(now, -20)); - ComponentDto project2 = newPrivateProjectDto(organizationDto); - IssueDoc project2Issue1 = newDoc(project2).setFuncCreationDate(addDays(now, -15)); - IssueDoc project2Issue2 = newDoc(project2).setFuncCreationDate(addDays(now, -30)); - indexIssues(project1Issue1, project1Issue2, project2Issue1, project2Issue2); - - // Search for issues of project 1 having less than 15 days - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of(project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), - project1Issue1.key()); - - // Search for issues of project 1 having less than 14 days and project 2 having less then 25 days - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( - project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), - project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), - project1Issue1.key(), project2Issue1.key()); - - // Search for issues of project 1 having less than 30 days - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( - project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), - project1Issue1.key(), project1Issue2.key()); - - // Search for issues of project 1 and project 2 having less than 5 days - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfterByProjectUuids(ImmutableMap.of( - project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true), - project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true)))); - } - - @Test - public void filter_by_severities() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setSeverity(Severity.INFO), - newDoc("I2", file).setSeverity(Severity.MAJOR)); - - assertThatSearchReturnsOnly(IssueQuery.builder().severities(asList(Severity.INFO, Severity.MAJOR)), "I1", "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().severities(singletonList(Severity.INFO)), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().severities(singletonList(Severity.BLOCKER))); - } - - @Test - public void facets_on_severities() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setSeverity(Severity.INFO), - newDoc("I2", file).setSeverity(Severity.INFO), - newDoc("I3", file).setSeverity(Severity.MAJOR)); - - assertThatFacetHasOnly(IssueQuery.builder(), "severities", entry("INFO", 2L), entry("MAJOR", 1L)); - } - - @Test - public void filter_by_statuses() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setStatus(Issue.STATUS_CLOSED), - newDoc("I2", file).setStatus(Issue.STATUS_OPEN)); - - assertThatSearchReturnsOnly(IssueQuery.builder().statuses(asList(Issue.STATUS_CLOSED, Issue.STATUS_OPEN)), "I1", "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().statuses(singletonList(Issue.STATUS_CLOSED)), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().statuses(singletonList(Issue.STATUS_CONFIRMED))); - } - - @Test - public void facets_on_statuses() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setStatus(Issue.STATUS_CLOSED), - newDoc("I2", file).setStatus(Issue.STATUS_CLOSED), - newDoc("I3", file).setStatus(Issue.STATUS_OPEN)); - - assertThatFacetHasOnly(IssueQuery.builder(), "statuses", entry("CLOSED", 2L), entry("OPEN", 1L)); - } - - @Test - public void filter_by_resolutions() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), - newDoc("I2", file).setResolution(Issue.RESOLUTION_FIXED)); - - assertThatSearchReturnsOnly(IssueQuery.builder().resolutions(asList(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_FIXED)), "I1", "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().resolutions(singletonList(Issue.RESOLUTION_FALSE_POSITIVE)), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().resolutions(singletonList(Issue.RESOLUTION_REMOVED))); - } - - @Test - public void facets_on_resolutions() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), - newDoc("I2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), - newDoc("I3", file).setResolution(Issue.RESOLUTION_FIXED)); - - assertThatFacetHasOnly(IssueQuery.builder(), "resolutions", entry("FALSE-POSITIVE", 2L), entry("FIXED", 1L)); - } - - @Test - public void filter_by_resolved() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED), - newDoc("I2", file).setStatus(Issue.STATUS_OPEN).setResolution(null), - newDoc("I3", file).setStatus(Issue.STATUS_OPEN).setResolution(null)); - - assertThatSearchReturnsOnly(IssueQuery.builder().resolved(true), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().resolved(false), "I2", "I3"); - assertThatSearchReturnsOnly(IssueQuery.builder().resolved(null), "I1", "I2", "I3"); - } - - @Test - public void filter_by_rules() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - RuleDefinitionDto ruleDefinitionDto = newRule(); - db.rules().insert(ruleDefinitionDto); - - indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId())); - - assertThatSearchReturnsOnly(IssueQuery.builder().rules(singletonList(ruleDefinitionDto)), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().rules(singletonList(new RuleDefinitionDto().setId(-1)))); - } - - @Test - public void filter_by_languages() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - RuleDefinitionDto ruleDefinitionDto = newRule(); - db.rules().insert(ruleDefinitionDto); - - indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId()).setLanguage("xoo")); - - assertThatSearchReturnsOnly(IssueQuery.builder().languages(singletonList("xoo")), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().languages(singletonList("unknown"))); - } - - @Test - public void facets_on_languages() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - RuleDefinitionDto ruleDefinitionDto = newRule(); - db.rules().insert(ruleDefinitionDto); - - indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId()).setLanguage("xoo")); - - assertThatFacetHasOnly(IssueQuery.builder(), "languages", entry("xoo", 1L)); - } - - @Test - public void filter_by_assignees() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAssigneeUuid("steph-uuid"), - newDoc("I2", file).setAssigneeUuid("marcel-uuid"), - newDoc("I3", file).setAssigneeUuid(null)); - - assertThatSearchReturnsOnly(IssueQuery.builder().assigneeUuids(singletonList("steph-uuid")), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().assigneeUuids(asList("steph-uuid", "marcel-uuid")), "I1", "I2"); - assertThatSearchReturnsEmpty(IssueQuery.builder().assigneeUuids(singletonList("unknown"))); - } - - @Test - public void facets_on_assignees() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAssigneeUuid("steph-uuid"), - newDoc("I2", file).setAssigneeUuid("marcel-uuid"), - newDoc("I3", file).setAssigneeUuid("marcel-uuid"), - newDoc("I4", file).setAssigneeUuid(null)); - - assertThatFacetHasOnly(IssueQuery.builder(), "assignees", entry("steph-uuid", 1L), entry("marcel-uuid", 2L), entry("", 1L)); - } - - @Test - public void facets_on_assignees_supports_dashes() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAssigneeUuid("j-b-uuid"), - newDoc("I2", file).setAssigneeUuid("marcel-uuid"), - newDoc("I3", file).setAssigneeUuid("marcel-uuid"), - newDoc("I4", file).setAssigneeUuid(null)); - - assertThatFacetHasOnly(IssueQuery.builder().assigneeUuids(singletonList("j-b")), - "assignees", entry("j-b-uuid", 1L), entry("marcel-uuid", 2L), entry("", 1L)); - } - - @Test - public void filter_by_assigned() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAssigneeUuid("steph-uuid"), - newDoc("I2", file).setAssigneeUuid(null), - newDoc("I3", file).setAssigneeUuid(null)); - - assertThatSearchReturnsOnly(IssueQuery.builder().assigned(true), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().assigned(false), "I2", "I3"); - assertThatSearchReturnsOnly(IssueQuery.builder().assigned(null), "I1", "I2", "I3"); - } - - @Test - public void filter_by_authors() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAuthorLogin("steph"), - newDoc("I2", file).setAuthorLogin("marcel"), - newDoc("I3", file).setAssigneeUuid(null)); - - assertThatSearchReturnsOnly(IssueQuery.builder().authors(singletonList("steph")), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().authors(asList("steph", "marcel")), "I1", "I2"); - assertThatSearchReturnsEmpty(IssueQuery.builder().authors(singletonList("unknown"))); - } - - @Test - public void facets_on_authors() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setAuthorLogin("steph"), - newDoc("I2", file).setAuthorLogin("marcel"), - newDoc("I3", file).setAuthorLogin("marcel"), - newDoc("I4", file).setAuthorLogin(null)); - - assertThatFacetHasOnly(IssueQuery.builder(), "authors", entry("steph", 1L), entry("marcel", 2L)); - } - - @Test - public void filter_by_created_after() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), - newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); - - assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-19")), "I1", "I2"); - // Lower bound is included - assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-20")), "I1", "I2"); - assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-21")), "I2"); - assertThatSearchReturnsEmpty(IssueQuery.builder().createdAfter(parseDate("2014-09-25"))); - } - - @Test - public void filter_by_created_before() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), - newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); - - assertThatSearchReturnsEmpty(IssueQuery.builder().createdBefore(parseDate("2014-09-19"))); - // Upper bound is excluded - assertThatSearchReturnsEmpty(IssueQuery.builder().createdBefore(parseDate("2014-09-20"))); - assertThatSearchReturnsOnly(IssueQuery.builder().createdBefore(parseDate("2014-09-21")), "I1"); - assertThatSearchReturnsOnly(IssueQuery.builder().createdBefore(parseDate("2014-09-25")), "I1", "I2"); - } - - @Test - public void filter_by_created_after_and_before() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), - newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); - - // 19 < createdAt < 25 - assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-19")).createdBefore(parseDate("2014-09-25")), - "I1", "I2"); - - // 20 < createdAt < 25: excludes first issue - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-25")), "I1", "I2"); - - // 21 < createdAt < 25 - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-25")), "I2"); - - // 21 < createdAt < 24 - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-24")), "I2"); - - // 21 < createdAt < 23: excludes second issue - assertThatSearchReturnsEmpty(IssueQuery.builder() - .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-23"))); - - // 19 < createdAt < 21: only first issue - assertThatSearchReturnsOnly(IssueQuery.builder() - .createdAfter(parseDate("2014-09-19")).createdBefore(parseDate("2014-09-21")), "I1"); - - // 20 < createdAt < 20: exception - expectedException.expect(IllegalArgumentException.class); - underTest.search(IssueQuery.builder() - .createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-20")) - .build(), new SearchOptions()); - } - - @Test - public void filter_by_create_after_and_before_take_into_account_timezone() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-20T00:00:00+0100")), - newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100"))); - - assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDateTime("2014-09-19T23:00:00+0000")).createdBefore(parseDateTime("2014-09-22T23:00:01+0000")), - "I1", "I2"); - - assertThatSearchReturnsEmpty(IssueQuery.builder().createdAfter(parseDateTime("2014-09-19T23:00:01+0000")).createdBefore(parseDateTime("2014-09-22T23:00:00+0000"))); - } - - @Test - public void filter_by_created_before_must_be_lower_than_after() { - try { - underTest.search(IssueQuery.builder().createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-19")).build(), - new SearchOptions()); - Fail.failBecauseExceptionWasNotThrown(IllegalArgumentException.class); - } catch (IllegalArgumentException exception) { - assertThat(exception.getMessage()).isEqualTo("Start bound cannot be larger or equal to end bound"); - } - } - - @Test - public void fail_if_created_before_equals_created_after() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Start bound cannot be larger or equal to end bound"); - - underTest.search(IssueQuery.builder().createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-20")).build(), new SearchOptions()); - } - - @Test - public void filter_by_created_after_must_not_be_in_future() { - try { - underTest.search(IssueQuery.builder().createdAfter(new Date(Long.MAX_VALUE)).build(), new SearchOptions()); - Fail.failBecauseExceptionWasNotThrown(IllegalArgumentException.class); - } catch (IllegalArgumentException exception) { - assertThat(exception.getMessage()).isEqualTo("Start bound cannot be in the future"); - } - } - - @Test - public void filter_by_created_at() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues(newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20"))); - - assertThatSearchReturnsOnly(IssueQuery.builder().createdAt(parseDate("2014-09-20")), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().createdAt(parseDate("2014-09-21"))); - } - - @Test - public void facet_on_created_at_with_less_than_20_days() { - SearchOptions options = fixtureForCreatedAtFacet(); - - IssueQuery query = IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) - .createdBefore(parseDateTime("2014-09-08T00:00:00+0100")) - .checkAuthorization(false) - .build(); - SearchResponse result = underTest.search(query, options); - Map buckets = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(buckets).containsOnly( - entry("2014-08-31", 0L), - entry("2014-09-01", 2L), - entry("2014-09-02", 1L), - entry("2014-09-03", 0L), - entry("2014-09-04", 0L), - entry("2014-09-05", 1L), - entry("2014-09-06", 0L), - entry("2014-09-07", 0L)); - } - - @Test - public void facet_on_created_at_with_less_than_20_weeks() { - SearchOptions options = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) - .createdBefore(parseDateTime("2014-09-21T00:00:00+0100")).build(), - options); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2014-08-25", 0L), - entry("2014-09-01", 4L), - entry("2014-09-08", 0L), - entry("2014-09-15", 1L)); - } - - @Test - public void facet_on_created_at_with_less_than_20_months() { - SearchOptions options = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) - .createdBefore(parseDateTime("2015-01-19T00:00:00+0100")).build(), - options); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2014-08-01", 0L), - entry("2014-09-01", 5L), - entry("2014-10-01", 0L), - entry("2014-11-01", 0L), - entry("2014-12-01", 0L), - entry("2015-01-01", 1L)); - } - - @Test - public void facet_on_created_at_with_more_than_20_months() { - SearchOptions options = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2011-01-01T00:00:00+0100")) - .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), - options); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2010-01-01", 0L), - entry("2011-01-01", 1L), - entry("2012-01-01", 0L), - entry("2013-01-01", 0L), - entry("2014-01-01", 5L), - entry("2015-01-01", 1L)); - } - - @Test - public void facet_on_created_at_with_one_day() { - SearchOptions options = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2014-09-01T00:00:00-0100")) - .createdBefore(parseDateTime("2014-09-02T00:00:00-0100")).build(), - options); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2014-09-01", 2L)); - } - - @Test - public void facet_on_created_at_with_bounds_outside_of_data() { - SearchOptions options = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdAfter(parseDateTime("2009-01-01T00:00:00+0100")) - .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")) - .build(), options); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2008-01-01", 0L), - entry("2009-01-01", 0L), - entry("2010-01-01", 0L), - entry("2011-01-01", 1L), - entry("2012-01-01", 0L), - entry("2013-01-01", 0L), - entry("2014-01-01", 5L), - entry("2015-01-01", 1L)); - } - - @Test - public void facet_on_created_at_without_start_bound() { - SearchOptions searchOptions = fixtureForCreatedAtFacet(); - - SearchResponse result = underTest.search(IssueQuery.builder() - .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), - searchOptions); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).containsOnly( - entry("2011-01-01", 1L), - entry("2012-01-01", 0L), - entry("2013-01-01", 0L), - entry("2014-01-01", 5L), - entry("2015-01-01", 1L)); - } - - @Test - public void facet_on_created_at_without_issues() { - SearchOptions searchOptions = new SearchOptions().addFacets("createdAt"); - - SearchResponse result = underTest.search(IssueQuery.builder().build(), searchOptions); - Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); - assertThat(createdAt).isNull(); - } - - private SearchOptions fixtureForCreatedAtFacet() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - IssueDoc issue0 = newDoc("ISSUE0", file).setFuncCreationDate(parseDateTime("2011-04-25T00:05:13+0000")); - IssueDoc issue1 = newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-01T12:34:56+0100")); - IssueDoc issue2 = newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-01T10:46:00-1200")); - IssueDoc issue3 = newDoc("I3", file).setFuncCreationDate(parseDateTime("2014-09-02T23:34:56+1200")); - IssueDoc issue4 = newDoc("I4", file).setFuncCreationDate(parseDateTime("2014-09-05T12:34:56+0100")); - IssueDoc issue5 = newDoc("I5", file).setFuncCreationDate(parseDateTime("2014-09-20T12:34:56+0100")); - IssueDoc issue6 = newDoc("I6", file).setFuncCreationDate(parseDateTime("2015-01-18T12:34:56+0100")); - - indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6); - - return new SearchOptions().addFacets("createdAt"); - } - - @Test - public void paging() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - for (int i = 0; i < 12; i++) { - indexIssues(newDoc("I" + i, file)); - } - - IssueQuery.Builder query = IssueQuery.builder(); - // There are 12 issues in total, with 10 issues per page, the page 2 should only contain 2 elements - SearchResponse result = underTest.search(query.build(), new SearchOptions().setPage(2, 10)); - assertThat(result.getHits().hits()).hasSize(2); - assertThat(result.getHits().getTotalHits()).isEqualTo(12); - - result = underTest.search(IssueQuery.builder().build(), new SearchOptions().setOffset(0).setLimit(5)); - assertThat(result.getHits().hits()).hasSize(5); - assertThat(result.getHits().getTotalHits()).isEqualTo(12); - - result = underTest.search(IssueQuery.builder().build(), new SearchOptions().setOffset(2).setLimit(0)); - assertThat(result.getHits().hits()).hasSize(10); - assertThat(result.getHits().getTotalHits()).isEqualTo(12); - } - - @Test - public void search_with_max_limit() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - List issues = new ArrayList<>(); - for (int i = 0; i < 500; i++) { - String key = "I" + i; - issues.add(newDoc(key, file)); - } - indexIssues(issues.toArray(new IssueDoc[] {})); - - IssueQuery.Builder query = IssueQuery.builder(); - SearchResponse result = underTest.search(query.build(), new SearchOptions().setLimit(Integer.MAX_VALUE)); - assertThat(result.getHits().hits()).hasSize(SearchOptions.MAX_LIMIT); - } - - @Test - public void sort_by_status() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setStatus(Issue.STATUS_OPEN), - newDoc("I2", file).setStatus(Issue.STATUS_CLOSED), - newDoc("I3", file).setStatus(Issue.STATUS_REOPENED)); - - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(true); - assertThatSearchReturnsOnly(query, "I2", "I1", "I3"); - - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(false); - assertThatSearchReturnsOnly(query, "I3", "I1", "I2"); - } - - @Test - public void sort_by_severity() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setSeverity(Severity.BLOCKER), - newDoc("I2", file).setSeverity(Severity.INFO), - newDoc("I3", file).setSeverity(Severity.MINOR), - newDoc("I4", file).setSeverity(Severity.CRITICAL), - newDoc("I5", file).setSeverity(Severity.MAJOR)); - - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(true); - assertThatSearchReturnsOnly(query, "I2", "I3", "I5", "I4", "I1"); - - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(false); - assertThatSearchReturnsOnly(query, "I1", "I4", "I5", "I3", "I2"); - } - - @Test - public void sort_by_creation_date() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-24T00:00:00+0100"))); - - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(true); - SearchResponse result = underTest.search(query.build(), new SearchOptions()); - assertThatSearchReturnsOnly(query, "I1", "I2"); - - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(false); - assertThatSearchReturnsOnly(query, "I2", "I1"); - } - - @Test - public void sort_by_update_date() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncUpdateDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("I2", file).setFuncUpdateDate(parseDateTime("2014-09-24T00:00:00+0100"))); - - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(true); - SearchResponse result = underTest.search(query.build(), new SearchOptions()); - assertThatSearchReturnsOnly(query, "I1", "I2"); - - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(false); - assertThatSearchReturnsOnly(query, "I2", "I1"); - } - - @Test - public void sort_by_close_date() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - - indexIssues( - newDoc("I1", file).setFuncCloseDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("I2", file).setFuncCloseDate(parseDateTime("2014-09-24T00:00:00+0100")), - newDoc("I3", file).setFuncCloseDate(null)); - - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(true); - SearchResponse result = underTest.search(query.build(), new SearchOptions()); - assertThatSearchReturnsOnly(query, "I3", "I1", "I2"); - - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(false); - assertThatSearchReturnsOnly(query, "I2", "I1", "I3"); - } - - @Test - public void sort_by_file_and_line() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file1 = newFileDto(project, null, "F1").setPath("src/main/xoo/org/sonar/samples/File.xoo"); - ComponentDto file2 = newFileDto(project, null, "F2").setPath("src/main/xoo/org/sonar/samples/File2.xoo"); - - indexIssues( - // file F1 - newDoc("F1_2", file1).setLine(20), - newDoc("F1_1", file1).setLine(null), - newDoc("F1_3", file1).setLine(25), - - // file F2 - newDoc("F2_1", file2).setLine(9), - newDoc("F2_2", file2).setLine(109), - // two issues on the same line -> sort by key - newDoc("F2_3", file2).setLine(109)); - - // ascending sort -> F1 then F2. Line "0" first. - IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(true); - assertThatSearchReturnsOnly(query, "F1_1", "F1_2", "F1_3", "F2_1", "F2_2", "F2_3"); - - // descending sort -> F2 then F1 - query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(false); - assertThatSearchReturnsOnly(query, "F2_3", "F2_2", "F2_1", "F1_3", "F1_2", "F1_1"); - } - - @Test - public void default_sort_is_by_creation_date_then_project_then_file_then_line_then_issue_key() { - OrganizationDto organizationDto = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(organizationDto, "P1"); - ComponentDto file1 = newFileDto(project1, null, "F1").setPath("src/main/xoo/org/sonar/samples/File.xoo"); - ComponentDto file2 = newFileDto(project1, null, "F2").setPath("src/main/xoo/org/sonar/samples/File2.xoo"); - - ComponentDto project2 = newPrivateProjectDto(organizationDto, "P2"); - ComponentDto file3 = newFileDto(project2, null, "F3").setPath("src/main/xoo/org/sonar/samples/File3.xoo"); - - indexIssues( - // file F1 from project P1 - newDoc("F1_1", file1).setLine(20).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("F1_2", file1).setLine(null).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("F1_3", file1).setLine(25).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - - // file F2 from project P1 - newDoc("F2_1", file2).setLine(9).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - newDoc("F2_2", file2).setLine(109).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - // two issues on the same line -> sort by key - newDoc("F2_3", file2).setLine(109).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), - - // file F3 from project P2 - newDoc("F3_1", file3).setLine(20).setFuncCreationDate(parseDateTime("2014-09-24T00:00:00+0100")), - newDoc("F3_2", file3).setLine(20).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100"))); - - assertThatSearchReturnsOnly(IssueQuery.builder(), "F3_1", "F1_2", "F1_1", "F1_3", "F2_1", "F2_2", "F2_3", "F3_2"); - } - - @Test - public void authorized_issues_on_groups() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(org); - ComponentDto project2 = newPrivateProjectDto(org); - ComponentDto project3 = newPrivateProjectDto(org); - ComponentDto file1 = newFileDto(project1, null); - ComponentDto file2 = newFileDto(project2, null); - ComponentDto file3 = newFileDto(project3, null); - GroupDto group1 = newGroupDto(); - GroupDto group2 = newGroupDto(); - - // project1 can be seen by group1 - indexIssue(newDoc("I1", file1)); - authorizationIndexerTester.allowOnlyGroup(project1, group1); - // project2 can be seen by group2 - indexIssue(newDoc("I2", file2)); - authorizationIndexerTester.allowOnlyGroup(project2, group2); - // project3 can be seen by nobody - indexIssue(newDoc("I3", file3)); - - userSessionRule.logIn().setGroups(group1); - assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); - - userSessionRule.logIn().setGroups(group2); - assertThatSearchReturnsOnly(IssueQuery.builder(), "I2"); - - userSessionRule.logIn().setGroups(group1, group2); - assertThatSearchReturnsOnly(IssueQuery.builder(), "I1", "I2"); - - GroupDto otherGroup = newGroupDto(); - userSessionRule.logIn().setGroups(otherGroup); - assertThatSearchReturnsEmpty(IssueQuery.builder()); - - userSessionRule.logIn().setGroups(group1, group2); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project3.uuid()))); - } - - @Test - public void authorized_issues_on_user() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project1 = newPrivateProjectDto(org); - ComponentDto project2 = newPrivateProjectDto(org); - ComponentDto project3 = newPrivateProjectDto(org); - ComponentDto file1 = newFileDto(project1, null); - ComponentDto file2 = newFileDto(project2, null); - ComponentDto file3 = newFileDto(project3, null); - UserDto user1 = newUserDto(); - UserDto user2 = newUserDto(); - - // project1 can be seen by john, project2 by max, project3 cannot be seen by anyone - indexIssue(newDoc("I1", file1)); - authorizationIndexerTester.allowOnlyUser(project1, user1); - indexIssue(newDoc("I2", file2)); - authorizationIndexerTester.allowOnlyUser(project2, user2); - indexIssue(newDoc("I3", file3)); - - userSessionRule.logIn(user1); - assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); - assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(asList(project3.getDbKey()))); - - userSessionRule.logIn(user2); - assertThatSearchReturnsOnly(IssueQuery.builder(), "I2"); - - // another user - userSessionRule.logIn(newUserDto()); - assertThatSearchReturnsEmpty(IssueQuery.builder()); - } - - @Test - public void root_user_is_authorized_to_access_all_issues() { - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - indexIssue(newDoc("I1", project)); - userSessionRule.logIn().setRoot(); - - assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); - } - - @Test - public void list_tags() { - RuleDefinitionDto r1 = db.rules().insert(); - RuleDefinitionDto r2 = db.rules().insert(); - ruleIndexer.commitAndIndex(db.getSession(), asList(r1.getId(), r2.getId())); - - OrganizationDto org = db.organizations().insert(); - OrganizationDto anotherOrg = db.organizations().insert(); - ComponentDto project = newPrivateProjectDto(newOrganizationDto()); - ComponentDto file = newFileDto(project, null); - indexIssues( - newDoc("I42", file).setOrganizationUuid(anotherOrg.getUuid()).setRuleId(r1.getId()).setTags(of("another")), - newDoc("I1", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention", "java8", "bug")), - newDoc("I2", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention", "bug")), - newDoc("I3", file).setOrganizationUuid(org.getUuid()).setRuleId(r2.getId()), - newDoc("I4", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention"))); - - assertThat(underTest.listTags(org, null, 100)).containsOnly("convention", "java8", "bug"); - assertThat(underTest.listTags(org, null, 2)).containsOnly("bug", "convention"); - assertThat(underTest.listTags(org, "vent", 100)).containsOnly("convention"); - assertThat(underTest.listTags(org, null, 1)).containsOnly("bug"); - assertThat(underTest.listTags(org, null, 100)).containsOnly("convention", "java8", "bug"); - assertThat(underTest.listTags(org, "invalidRegexp[", 100)).isEmpty(); - assertThat(underTest.listTags(null, null, 100)).containsExactlyInAnyOrder("another", "convention", "java8", "bug"); - } - - @Test - public void fail_to_list_tags_when_size_greater_than_500() { - OrganizationDto organization = db.organizations().insert(); - - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Page size must be lower than or equals to 500"); - - underTest.listTags(organization, null, 501); - } - - @Test - public void test_listAuthors() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("issue1", project).setAuthorLogin("luke.skywalker"), - newDoc("issue2", project).setAuthorLogin("luke@skywalker.name"), - newDoc("issue3", project).setAuthorLogin(null), - newDoc("issue4", project).setAuthorLogin("anakin@skywalker.name")); - IssueQuery query = IssueQuery.builder() - .checkAuthorization(false) - .build(); - - assertThat(underTest.listAuthors(query, null, 5)).containsExactly("anakin@skywalker.name", "luke.skywalker", "luke@skywalker.name"); - assertThat(underTest.listAuthors(query, null, 2)).containsExactly("anakin@skywalker.name", "luke.skywalker"); - assertThat(underTest.listAuthors(query, "uke", 5)).containsExactly("luke.skywalker", "luke@skywalker.name"); - assertThat(underTest.listAuthors(query, null, 1)).containsExactly("anakin@skywalker.name"); - assertThat(underTest.listAuthors(query, null, Integer.MAX_VALUE)).containsExactly("anakin@skywalker.name", "luke.skywalker", "luke@skywalker.name"); - } - - @Test - public void listAuthors_escapes_regexp_special_characters() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("issue1", project).setAuthorLogin("name++")); - IssueQuery query = IssueQuery.builder() - .checkAuthorization(false) - .build(); - - assertThat(underTest.listAuthors(query, "invalidRegexp[", 5)).isEmpty(); - assertThat(underTest.listAuthors(query, "nam+", 5)).isEmpty(); - assertThat(underTest.listAuthors(query, "name+", 5)).containsExactly("name++"); - assertThat(underTest.listAuthors(query, ".*", 5)).isEmpty(); - } - - @Test - public void filter_by_organization() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto projectInOrg1 = newPrivateProjectDto(org1); - OrganizationDto org2 = newOrganizationDto(); - ComponentDto projectInOrg2 = newPrivateProjectDto(org2); - - indexIssues(newDoc("issueInOrg1", projectInOrg1), newDoc("issue1InOrg2", projectInOrg2), newDoc("issue2InOrg2", projectInOrg2)); - - verifyOrganizationFilter(org1.getUuid(), "issueInOrg1"); - verifyOrganizationFilter(org2.getUuid(), "issue1InOrg2", "issue2InOrg2"); - verifyOrganizationFilter("does_not_exist"); - } - - @Test - public void filter_by_organization_and_project() { - OrganizationDto org1 = newOrganizationDto(); - ComponentDto projectInOrg1 = newPrivateProjectDto(org1); - OrganizationDto org2 = newOrganizationDto(); - ComponentDto projectInOrg2 = newPrivateProjectDto(org2); - - indexIssues(newDoc("issueInOrg1", projectInOrg1), newDoc("issue1InOrg2", projectInOrg2), newDoc("issue2InOrg2", projectInOrg2)); - - // no conflict - IssueQuery.Builder query = IssueQuery.builder().organizationUuid(org1.getUuid()).projectUuids(singletonList(projectInOrg1.uuid())); - assertThatSearchReturnsOnly(query, "issueInOrg1"); - - // conflict - query = IssueQuery.builder().organizationUuid(org1.getUuid()).projectUuids(singletonList(projectInOrg2.uuid())); - assertThatSearchReturnsEmpty(query); - } - - @Test - public void countTags() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("issue1", project).setTags(ImmutableSet.of("convention", "java8", "bug")), - newDoc("issue2", project).setTags(ImmutableSet.of("convention", "bug")), - newDoc("issue3", project).setTags(emptyList()), - newDoc("issue4", project).setTags(ImmutableSet.of("convention", "java8", "bug")).setResolution(Issue.RESOLUTION_FIXED), - newDoc("issue5", project).setTags(ImmutableSet.of("convention"))); - - assertThat(underTest.countTags(projectQuery(project.uuid()), 5)).containsOnly(entry("convention", 3L), entry("bug", 2L), entry("java8", 1L)); - assertThat(underTest.countTags(projectQuery(project.uuid()), 2)).contains(entry("convention", 3L), entry("bug", 2L)).doesNotContainEntry("java8", 1L); - assertThat(underTest.countTags(projectQuery("other"), 10)).isEmpty(); - } - - @Test - public void searchBranchStatistics() { - ComponentDto project = db.components().insertMainBranch(); - ComponentDto branch1 = db.components().insertProjectBranch(project); - ComponentDto branch2 = db.components().insertProjectBranch(project); - ComponentDto branch3 = db.components().insertProjectBranch(project); - ComponentDto fileOnBranch3 = db.components().insertComponent(newFileDto(branch3)); - indexIssues(newDoc(project), - newDoc(branch1).setType(BUG).setResolution(null), newDoc(branch1).setType(VULNERABILITY).setResolution(null), newDoc(branch1).setType(CODE_SMELL).setResolution(null), - newDoc(branch1).setType(CODE_SMELL).setResolution(RESOLUTION_FIXED), - newDoc(branch3).setType(CODE_SMELL).setResolution(null), newDoc(branch3).setType(CODE_SMELL).setResolution(null), - newDoc(fileOnBranch3).setType(CODE_SMELL).setResolution(null), newDoc(fileOnBranch3).setType(CODE_SMELL).setResolution(RESOLUTION_FIXED)); - - List branchStatistics = underTest.searchBranchStatistics(project.uuid(), asList(branch1.uuid(), branch2.uuid(), branch3.uuid())); - - assertThat(branchStatistics).extracting(BranchStatistics::getBranchUuid, BranchStatistics::getBugs, BranchStatistics::getVulnerabilities, BranchStatistics::getCodeSmells) - .containsExactlyInAnyOrder( - tuple(branch1.uuid(), 1L, 1L, 1L), - tuple(branch3.uuid(), 0L, 0L, 3L)); - } - - @Test - public void searchBranchStatistics_on_many_branches() { - ComponentDto project = db.components().insertMainBranch(); - List branchUuids = new ArrayList<>(); - List expectedResult = new ArrayList<>(); - IntStream.range(0, 15).forEach(i -> { - ComponentDto branch = db.components().insertProjectBranch(project); - addIssues(branch, 1 + i, 2 + i, 3 + i); - expectedResult.add(tuple(branch.uuid(), 1L + i, 2L + i, 3L + i)); - branchUuids.add(branch.uuid()); - }); - - List branchStatistics = underTest.searchBranchStatistics(project.uuid(), branchUuids); - - assertThat(branchStatistics) - .extracting(BranchStatistics::getBranchUuid, BranchStatistics::getBugs, BranchStatistics::getVulnerabilities, BranchStatistics::getCodeSmells) - .hasSize(15) - .containsAll(expectedResult); - } - - @Test - public void searchBranchStatistics_on_empty_list() { - ComponentDto project = db.components().insertMainBranch(); - - assertThat(underTest.searchBranchStatistics(project.uuid(), emptyList())).isEmpty(); - assertThat(underTest.searchBranchStatistics(project.uuid(), singletonList("unknown"))).isEmpty(); - } - - @Test - public void test_getOwaspTop10Report_dont_count_vulnerabilities_from_other_projects() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - ComponentDto another = newPrivateProjectDto(org); - indexIssues( - newDoc("anotherProject", another).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), - newDoc("openvul1", project).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating) - .contains( - tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */)); - - } - - @Test - public void test_getOwaspTop10Report_dont_count_closed_vulnerabilities() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("openvul1", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR), - newDoc("notopenvul", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED) - .setSeverity(Severity.BLOCKER)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating) - .contains( - tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */)); - } - - @Test - public void test_getOwaspTop10Report_dont_count_old_vulnerabilities() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - // Previous vulnerabilities in projects that are not reanalyzed will have no owasp nor cwe attributes (not even 'unknown') - newDoc("openvulNotReindexed", project).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating) - .containsOnly( - tuple(0L, OptionalInt.empty())); - } - - @Test - public void test_getOwaspTop10Report_dont_count_hotspots_from_other_projects() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - ComponentDto another = newPrivateProjectDto(org); - indexIssues( - newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN), - newDoc("anotherProject", another).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots) - .contains( - tuple("a1", 1L /* openhotspot1 */)); - } - - @Test - public void test_getOwaspTop10Report_dont_count_closed_hotspots() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN), - newDoc("closedHotspot", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED) - .setResolution(Issue.RESOLUTION_FIXED)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots) - .contains( - tuple("a1", 1L /* openhotspot1 */)); - } - - @Test - public void test_getOwaspTop10Report_aggregation_no_cwe() { - List owaspTop10Report = indexIssuesAndAssertOwaspReport(false); - - assertThat(owaspTop10Report).allMatch(category -> category.getChildren().isEmpty()); - } - - @Test - public void test_getOwaspTop10Report_aggregation_with_cwe() { - List owaspTop10Report = indexIssuesAndAssertOwaspReport(true); - - Map> cweByOwasp = owaspTop10Report.stream() - .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren)); - - assertThat(cweByOwasp.get("a1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, - SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) - .containsExactlyInAnyOrder( - tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), - tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), - tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); - assertThat(cweByOwasp.get("a3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, - SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) - .containsExactlyInAnyOrder( - tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), - tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 1L /* toReviewHotspot */, 0L), - tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); - } - - private List indexIssuesAndAssertOwaspReport(boolean includeCwe) { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - ComponentDto another = newPrivateProjectDto(org); - indexIssues( - newDoc("openvul1", project).setOwaspTop10(asList("a1", "a3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.MAJOR), - newDoc("openvul2", project).setOwaspTop10(asList("a3", "a6")).setCwe(asList("123")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) - .setSeverity(Severity.MINOR), - newDoc("notowaspvul", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.CRITICAL), - newDoc("openhotspot1", project).setOwaspTop10(asList("a1", "a3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT) - .setStatus(Issue.STATUS_OPEN), - newDoc("openhotspot2", project).setOwaspTop10(asList("a3", "a6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REOPENED), - newDoc("toReviewHotspot", project).setOwaspTop10(asList("a5", "a3")).setCwe(asList("456")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_FIXED), - newDoc("WFHotspot", project).setOwaspTop10(asList("a3", "a8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_WONT_FIX), - newDoc("notowasphotspot", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); - - List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, includeCwe); - assertThat(owaspTop10Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, - SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) - .containsExactlyInAnyOrder( - tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), - tuple("a2", 0L, OptionalInt.empty(), 0L, 0L, 0L), - tuple("a3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */, 1L /* WFHotspot */), - tuple("a4", 0L, OptionalInt.empty(), 0L, 0L, 0L), - tuple("a5", 0L, OptionalInt.empty(), 0L, 1L/* toReviewHotspot */, 0L), - tuple("a6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* openhotspot2 */, 0L, 0L), - tuple("a7", 0L, OptionalInt.empty(), 0L, 0L, 0L), - tuple("a8", 0L, OptionalInt.empty(), 0L, 0L, 1L /* WFHotspot */), - tuple("a9", 0L, OptionalInt.empty(), 0L, 0L, 0L), - tuple("a10", 0L, OptionalInt.empty(), 0L, 0L, 0L), - tuple("unknown", 1L /* notowaspvul */, OptionalInt.of(4) /* CRITICAL = D */, 1L /* notowasphotspot */, 0L, 0L)); - return owaspTop10Report; - } - - @Test - public void test_getSansTop25Report_aggregation() { - OrganizationDto org = newOrganizationDto(); - ComponentDto project = newPrivateProjectDto(org); - indexIssues( - newDoc("openvul1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.MAJOR), - newDoc("openvul2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) - .setSeverity(Severity.MINOR), - newDoc("notopenvul", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED) - .setResolution(Issue.RESOLUTION_FIXED) - .setSeverity(Severity.BLOCKER), - newDoc("notsansvul", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.CRITICAL), - newDoc("openhotspot1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) - .setStatus(Issue.STATUS_OPEN), - newDoc("openhotspot2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) - .setStatus(Issue.STATUS_REOPENED), - newDoc("toReviewHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_FIXED), - newDoc("WFHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_WONT_FIX), - newDoc("notowasphotspot", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); - - List sansTop25Report = underTest.getSansTop25Report(project.uuid(), false, false); - assertThat(sansTop25Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, - SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) - .containsExactlyInAnyOrder( - tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), - tuple(SANS_TOP_25_RISKY_RESOURCE, 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */, - 1L /* WFHotspot */), - tuple(SANS_TOP_25_POROUS_DEFENSES, 1L /* openvul2 */, OptionalInt.of(2)/* MINOR = B */, 1L/* openhotspot2 */, 0L, 0L)); - - assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); - } - - @Test - public void test_getSansTop25Report_aggregation_on_portfolio() { - ComponentDto portfolio1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto portfolio2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); - ComponentDto project1 = db.components().insertPrivateProject(); - ComponentDto project2 = db.components().insertPrivateProject(); - - indexIssues( - newDoc("openvul1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.MAJOR), - newDoc("openvul2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) - .setSeverity(Severity.MINOR), - newDoc("notopenvul", project1).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED) - .setResolution(Issue.RESOLUTION_FIXED) - .setSeverity(Severity.BLOCKER), - newDoc("notsansvul", project2).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) - .setSeverity(Severity.CRITICAL), - newDoc("openhotspot1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) - .setStatus(Issue.STATUS_OPEN), - newDoc("openhotspot2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) - .setStatus(Issue.STATUS_REOPENED), - newDoc("toReviewHotspot", project1).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_FIXED), - newDoc("WFHotspot", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) - .setResolution(Issue.RESOLUTION_WONT_FIX), - newDoc("notowasphotspot", project1).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); - - indexView(portfolio1.uuid(), singletonList(project1.uuid())); - indexView(portfolio2.uuid(), singletonList(project2.uuid())); - - List sansTop25Report = underTest.getSansTop25Report(portfolio1.uuid(), true, false); - assertThat(sansTop25Report) - .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, - SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, - SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) - .containsExactlyInAnyOrder( - tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), - tuple(SANS_TOP_25_RISKY_RESOURCE, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L/* openhotspot1 */, 1L /* toReviewHotspot */, 0L), - tuple(SANS_TOP_25_POROUS_DEFENSES, 0L, OptionalInt.empty(), 0L, 0L, 0L)); - - assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); - } - - private void addIssues(ComponentDto component, int bugs, int vulnerabilities, int codeSmelles) { - List issues = new ArrayList<>(); - IntStream.range(0, bugs).forEach(b -> issues.add(newDoc(component).setType(BUG).setResolution(null))); - IntStream.range(0, vulnerabilities).forEach(v -> issues.add(newDoc(component).setType(VULNERABILITY).setResolution(null))); - IntStream.range(0, codeSmelles).forEach(c -> issues.add(newDoc(component).setType(CODE_SMELL).setResolution(null))); - indexIssues(issues.toArray(new IssueDoc[issues.size()])); - } - - private IssueQuery projectQuery(String projectUuid) { - return IssueQuery.builder().projectUuids(singletonList(projectUuid)).resolved(false).build(); - } - - private void verifyOrganizationFilter(String organizationUuid, String... expectedIssueKeys) { - IssueQuery.Builder query = IssueQuery.builder().organizationUuid(organizationUuid); - assertThatSearchReturnsOnly(query, expectedIssueKeys); - } - - private void indexIssues(IssueDoc... issues) { - issueIndexer.index(asList(issues).iterator()); - for (IssueDoc issue : issues) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(issue.projectUuid(), "TRK"); - access.allowAnyone(); - authorizationIndexerTester.allow(access); - } - } - - private void indexIssue(IssueDoc issue) { - issueIndexer.index(Iterators.singletonIterator(issue)); - } - - private void indexView(String viewUuid, List projects) { - viewIndexer.index(new ViewDoc().setUuid(viewUuid).setProjects(projects)); - } - - /** - * Execute the search request and return the document ids of results. - */ - private List searchAndReturnKeys(IssueQuery.Builder query) { - return Arrays.stream(underTest.search(query.build(), new SearchOptions()).getHits().getHits()) - .map(SearchHit::getId) - .collect(Collectors.toList()); - } - - private void assertThatSearchReturnsOnly(IssueQuery.Builder query, String... expectedIssueKeys) { - List keys = searchAndReturnKeys(query); - assertThat(keys).containsExactlyInAnyOrder(expectedIssueKeys); - } - - private void assertThatSearchReturnsEmpty(IssueQuery.Builder query) { - List keys = searchAndReturnKeys(query); - assertThat(keys).isEmpty(); - } - - private void assertThatFacetHasExactly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { - SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); - Facets facets = new Facets(result, system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly(facet); - assertThat(facets.get(facet)).containsExactly(expectedEntries); - } - - private void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { - SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); - Facets facets = new Facets(result, system2.getDefaultTimeZone()); - assertThat(facets.getNames()).containsOnly(facet); - assertThat(facets.get(facet)).containsOnly(expectedEntries); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java index 6ecf880a1dc..3c793a28e6e 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/index/IssueIndexerTest.java @@ -47,7 +47,7 @@ import org.sonar.server.es.EsTester; import org.sonar.server.es.IndexingResult; import org.sonar.server.es.ProjectIndexer; import org.sonar.server.permission.index.AuthorizationScope; -import org.sonar.server.permission.index.PermissionIndexerDao; +import org.sonar.server.permission.index.IndexPermissions; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; @@ -56,10 +56,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.rules.ExpectedException.none; import static org.sonar.db.component.ComponentTesting.newFileDto; import static org.sonar.server.issue.IssueDocTesting.newDoc; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE; -import static org.sonar.server.permission.index.AuthorizationTypeSupport.TYPE_AUTHORIZATION; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_POROUS_DEFENSES; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; public class IssueIndexerTest { @@ -91,9 +91,9 @@ public class IssueIndexerTest { assertThat(scope.getIndexType().getIndex()).isEqualTo(INDEX_TYPE_ISSUE.getIndex()); assertThat(scope.getIndexType().getType()).isEqualTo(TYPE_AUTHORIZATION); - Predicate projectPredicate = scope.getProjectPredicate(); - PermissionIndexerDao.Dto project = new PermissionIndexerDao.Dto("P1", Qualifiers.PROJECT); - PermissionIndexerDao.Dto file = new PermissionIndexerDao.Dto("F1", Qualifiers.FILE); + Predicate projectPredicate = scope.getProjectPredicate(); + IndexPermissions project = new IndexPermissions("P1", Qualifiers.PROJECT); + IndexPermissions file = new IndexPermissions("F1", Qualifiers.FILE); assertThat(projectPredicate.test(project)).isTrue(); assertThat(projectPredicate.test(file)).isFalse(); } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java deleted file mode 100644 index a9eaf3102fb..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java +++ /dev/null @@ -1,1477 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.IntStream; -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; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.organization.OrganizationTesting; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.Facets; -import org.sonar.server.es.SearchIdResult; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static com.google.common.collect.Lists.newArrayList; -import static com.google.common.collect.Sets.newHashSet; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; -import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; -import static org.sonar.api.measures.Metric.Level.ERROR; -import static org.sonar.api.measures.Metric.Level.OK; -import static org.sonar.api.measures.Metric.Level.WARN; -import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; -import static org.sonar.db.user.GroupTesting.newGroupDto; -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.INDEX_TYPE_PROJECT_MEASURES; -import static org.sonar.server.measure.index.ProjectMeasuresQuery.*; - -@RunWith(DataProviderRunner.class) -public class ProjectMeasuresIndexTest { - - private static final String MAINTAINABILITY_RATING = "sqale_rating"; - private static final String NEW_MAINTAINABILITY_RATING_KEY = "new_maintainability_rating"; - private static final String RELIABILITY_RATING = "reliability_rating"; - private static final String NEW_RELIABILITY_RATING = "new_reliability_rating"; - private static final String SECURITY_RATING = "security_rating"; - private static final String NEW_SECURITY_RATING = "new_security_rating"; - private static final String COVERAGE = "coverage"; - private static final String NEW_COVERAGE = "new_coverage"; - private static final String DUPLICATION = "duplicated_lines_density"; - private static final String NEW_DUPLICATION = "new_duplicated_lines_density"; - private static final String NCLOC = "ncloc"; - private static final String NEW_LINES = "new_lines"; - private static final String LANGUAGES = "languages"; - - private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); - private static final ComponentDto PROJECT1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-1").setName("Project 1").setDbKey("key-1"); - private static final ComponentDto PROJECT2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-2").setName("Project 2").setDbKey("key-2"); - private static final ComponentDto PROJECT3 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-3").setName("Project 3").setDbKey("key-3"); - private static final UserDto USER1 = newUserDto(); - private static final UserDto USER2 = newUserDto(); - private static final GroupDto GROUP1 = newGroupDto(); - private static final GroupDto GROUP2 = newGroupDto(); - - @Rule - public EsTester es = EsTester.create(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - - @DataProvider - public static Object[][] rating_metric_keys() { - return new Object[][]{{MAINTAINABILITY_RATING}, {NEW_MAINTAINABILITY_RATING_KEY}, {RELIABILITY_RATING}, {NEW_RELIABILITY_RATING}, {SECURITY_RATING}, {NEW_SECURITY_RATING}}; - } - - private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); - private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); - - @Test - public void return_empty_if_no_projects() { - assertNoResults(new ProjectMeasuresQuery()); - } - - @Test - public void default_sort_is_by_ascending_case_insensitive_name_then_by_key() { - ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); - ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); - ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); - ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); - index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); - - assertResults(new ProjectMeasuresQuery(), apache1, apache2, apachee, windows); - } - - @Test - public void sort_by_insensitive_name() { - ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows"); - ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee"); - ComponentDto apache = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache").setName("Apache"); - index(newDoc(windows), newDoc(apachee), newDoc(apache)); - - assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(true), apache, apachee, windows); - assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(false), windows, apachee, apache); - } - - @Test - public void sort_by_ncloc() { - index( - newDoc(PROJECT1, NCLOC, 15_000d), - newDoc(PROJECT2, NCLOC, 30_000d), - newDoc(PROJECT3, NCLOC, 1_000d)); - - assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), PROJECT3, PROJECT1, PROJECT2); - assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), PROJECT2, PROJECT1, PROJECT3); - } - - @Test - public void sort_by_a_metric_then_by_name_then_by_key() { - ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); - ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); - ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); - ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); - index( - newDoc(windows, NCLOC, 10_000d), - newDoc(apachee, NCLOC, 5_000d), - newDoc(apache1, NCLOC, 5_000d), - newDoc(apache2, NCLOC, 5_000d)); - - assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), apache1, apache2, apachee, windows); - assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), windows, apache1, apache2, apachee); - } - - @Test - public void sort_by_quality_gate_status() { - ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); - index( - newDoc(PROJECT1).setQualityGateStatus(OK.name()), - newDoc(PROJECT2).setQualityGateStatus(ERROR.name()), - newDoc(PROJECT3).setQualityGateStatus(WARN.name()), - newDoc(project4).setQualityGateStatus(OK.name())); - - assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), PROJECT1, project4, PROJECT3, PROJECT2); - assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), PROJECT2, PROJECT3, PROJECT1, project4); - } - - @Test - public void sort_by_quality_gate_status_then_by_name_then_by_key() { - ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); - ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); - ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); - ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); - index( - newDoc(windows).setQualityGateStatus(WARN.name()), - newDoc(apachee).setQualityGateStatus(OK.name()), - newDoc(apache1).setQualityGateStatus(OK.name()), - newDoc(apache2).setQualityGateStatus(OK.name())); - - assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), apache1, apache2, apachee, windows); - assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), windows, apache1, apache2, apachee); - } - - @Test - public void paginate_results() { - IntStream.rangeClosed(1, 9) - .forEach(i -> index(newDoc(newPrivateProjectDto(ORG, "P" + i)))); - - SearchIdResult result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().setPage(2, 3)); - - assertThat(result.getIds()).containsExactly("P4", "P5", "P6"); - assertThat(result.getTotal()).isEqualTo(9); - } - - @Test - public void filter_with_lower_than() { - index( - newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), - newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 80d)); - - assertResults(query, PROJECT1); - } - - @Test - public void filter_with_lower_than_or_equals() { - index( - newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), - newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)); - - assertResults(query, PROJECT1, PROJECT2); - } - - @Test - public void filter_with_greater_than() { - index( - newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), - newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 30_000d)); - assertResults(query, PROJECT2, PROJECT3); - - query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 100_000d)); - assertNoResults(query); - } - - @Test - public void filter_with_greater_than_or_equals() { - index( - newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), - newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 30_001d)); - assertResults(query, PROJECT2, PROJECT3); - - query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 100_000d)); - assertNoResults(query); - } - - @Test - public void filter_with_equals() { - index( - newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), - newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.EQ, 80d)); - - assertResults(query, PROJECT2); - } - - @Test - public void filter_on_no_data_with_several_projects() { - index( - newDoc(PROJECT1, NCLOC, 1d), - newDoc(PROJECT2, DUPLICATION, 80d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); - - assertResults(query, PROJECT1); - } - - @Test - public void filter_on_no_data_should_not_return_projects_with_data_and_other_measures() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); - index(newDoc(project, DUPLICATION, 80d, NCLOC, 1d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); - - assertNoResults(query); - } - - @Test - public void filter_on_no_data_should_not_return_projects_with_data() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); - index(newDoc(project, DUPLICATION, 80d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); - - assertNoResults(query); - } - - @Test - public void filter_on_no_data_should_return_projects_with_no_data() { - ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); - index(newDoc(project, NCLOC, 1d)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); - - assertResults(query, project); - } - - @Test - public void filter_on_several_metrics() { - index( - newDoc(PROJECT1, COVERAGE, 81d, NCLOC, 10_001d), - newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_001d), - newDoc(PROJECT3, COVERAGE, 79d, NCLOC, 10_000d)); - - ProjectMeasuresQuery esQuery = new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)) - .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 10_000d)) - .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 11_000d)); - assertResults(esQuery, PROJECT2); - } - - @Test - public void filter_on_quality_gate_status() { - index( - newDoc(PROJECT1).setQualityGateStatus(OK.name()), - newDoc(PROJECT2).setQualityGateStatus(OK.name()), - newDoc(PROJECT3).setQualityGateStatus(WARN.name())); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().setQualityGateStatus(OK); - assertResults(query, PROJECT1, PROJECT2); - } - - @Test - public void filter_on_languages() { - ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); - index( - newDoc(PROJECT1).setLanguages(singletonList("java")), - newDoc(PROJECT2).setLanguages(singletonList("xoo")), - newDoc(PROJECT3).setLanguages(singletonList("xoo")), - newDoc(project4).setLanguages(asList("", "java", "xoo"))); - - assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java", "xoo")), PROJECT1, PROJECT2, PROJECT3, project4); - assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java")), PROJECT1, project4); - assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("unknown"))); - } - - @Test - public void filter_on_query_text() { - ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); - ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); - ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); - ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); - index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); - - assertResults(new ProjectMeasuresQuery().setQueryText("windows"), windows); - assertResults(new ProjectMeasuresQuery().setQueryText("project2"), apachee); - assertResults(new ProjectMeasuresQuery().setQueryText("pAch"), apache1, apache2, apachee); - } - - @Test - public void filter_on_ids() { - index( - newDoc(PROJECT1), - newDoc(PROJECT2), - newDoc(PROJECT3)); - - ProjectMeasuresQuery query = new ProjectMeasuresQuery().setProjectUuids(newHashSet(PROJECT1.uuid(), PROJECT3.uuid())); - assertResults(query, PROJECT1, PROJECT3); - } - - @Test - public void filter_on_tags() { - index( - newDoc(PROJECT1).setTags(newArrayList("finance", "platform")), - newDoc(PROJECT2).setTags(newArrayList("marketing", "platform")), - newDoc(PROJECT3).setTags(newArrayList("finance", "language"))); - - assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance")), PROJECT1, PROJECT3); - assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "language")), PROJECT1, PROJECT3); - assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "marketing")), PROJECT1, PROJECT2, PROJECT3); - assertResults(new ProjectMeasuresQuery().setTags(newHashSet("marketing")), PROJECT2); - assertNoResults(new ProjectMeasuresQuery().setTags(newHashSet("tag 42"))); - } - - @Test - public void filter_on_organization() { - OrganizationDto org1 = OrganizationTesting.newOrganizationDto(); - OrganizationDto org2 = OrganizationTesting.newOrganizationDto(); - ComponentDto projectInOrg1 = ComponentTesting.newPrivateProjectDto(org1); - ComponentDto projectInOrg2 = ComponentTesting.newPrivateProjectDto(org2); - index(newDoc(projectInOrg1), newDoc(projectInOrg2)); - - ProjectMeasuresQuery query1 = new ProjectMeasuresQuery().setOrganizationUuid(org1.getUuid()); - assertResults(query1, projectInOrg1); - - ProjectMeasuresQuery query2 = new ProjectMeasuresQuery().setOrganizationUuid(org2.getUuid()); - assertResults(query2, projectInOrg2); - - ProjectMeasuresQuery query3 = new ProjectMeasuresQuery().setOrganizationUuid("another_org"); - assertNoResults(query3); - } - - @Test - public void return_only_projects_authorized_for_user() { - indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); - indexForUser(USER2, newDoc(PROJECT3)); - - userSession.logIn(USER1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); - } - - @Test - public void return_only_projects_authorized_for_user_groups() { - indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2)); - indexForGroup(GROUP2, newDoc(PROJECT3)); - - userSession.logIn().setGroups(GROUP1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); - } - - @Test - public void return_only_projects_authorized_for_user_and_groups() { - indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); - indexForGroup(GROUP1, newDoc(PROJECT3)); - - userSession.logIn(USER1).setGroups(GROUP1); - assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2, PROJECT3); - } - - @Test - public void anonymous_user_can_only_access_projects_authorized_for_anyone() { - index(newDoc(PROJECT1)); - indexForUser(USER1, newDoc(PROJECT2)); - - userSession.anonymous(); - assertResults(new ProjectMeasuresQuery(), PROJECT1); - } - - @Test - public void root_user_can_access_all_projects() { - indexForUser(USER1, newDoc(PROJECT1)); - // connecting with a root but not USER1 - userSession.logIn().setRoot(); - - assertResults(new ProjectMeasuresQuery(), PROJECT1); - } - - @Test - public void does_not_return_facet_when_no_facets_in_options() { - index( - newDoc(PROJECT1, NCLOC, 10d, COVERAGE_KEY, 30d, MAINTAINABILITY_RATING, 3d) - .setQualityGateStatus(OK.name())); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions()).getFacets(); - - assertThat(facets.getAll()).isEmpty(); - } - - @Test - public void facet_ncloc() { - index( - // 3 docs with ncloc<1K - newDoc(NCLOC, 0d), - newDoc(NCLOC, 0d), - newDoc(NCLOC, 999d), - // 2 docs with ncloc>=1K and ncloc<10K - newDoc(NCLOC, 1_000d), - newDoc(NCLOC, 9_999d), - // 4 docs with ncloc>=10K and ncloc<100K - newDoc(NCLOC, 10_000d), - newDoc(NCLOC, 10_000d), - newDoc(NCLOC, 11_000d), - newDoc(NCLOC, 99_000d), - // 2 docs with ncloc>=100K and ncloc<500K - newDoc(NCLOC, 100_000d), - newDoc(NCLOC, 499_000d), - // 5 docs with ncloc>= 500K - newDoc(NCLOC, 500_000d), - newDoc(NCLOC, 100_000_000d), - newDoc(NCLOC, 500_000d), - newDoc(NCLOC, 1_000_000d), - newDoc(NCLOC, 100_000_000_000d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); - - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 3L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 4L), - entry("100000.0-500000.0", 2L), - entry("500000.0-*", 5L)); - } - - @Test - public void facet_ncloc_is_sticky() { - index( - // 1 docs with ncloc<1K - newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), - // 2 docs with ncloc>=1K and ncloc<10K - newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), - newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), - // 3 docs with ncloc>=10K and ncloc<100K - newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), - newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), - newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), - // 2 docs with ncloc>=100K and ncloc<500K - newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), - newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 0d), - // 1 docs with ncloc>= 500K - newDoc(NCLOC, 501_000d, COVERAGE, 81d, DUPLICATION, 20d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 10_000d)) - .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), - new SearchOptions().addFacets(NCLOC, COVERAGE)).getFacets(); - - // Sticky facet on ncloc does not take into account ncloc filter - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 1L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 3L), - entry("100000.0-500000.0", 2L), - entry("500000.0-*", 0L)); - // But facet on coverage does well take into into filters - assertThat(facets.get(COVERAGE)).containsOnly( - entry("NO_DATA", 0L), - entry("*-30.0", 3L), - entry("30.0-50.0", 0L), - entry("50.0-70.0", 0L), - entry("70.0-80.0", 0L), - entry("80.0-*", 0L)); - } - - @Test - public void facet_ncloc_contains_only_projects_authorized_for_user() { - // User can see these projects - indexForUser(USER1, - // docs with ncloc<1K - newDoc(NCLOC, 0d), - newDoc(NCLOC, 100d), - newDoc(NCLOC, 999d), - // docs with ncloc>=1K and ncloc<10K - newDoc(NCLOC, 1_000d), - newDoc(NCLOC, 9_999d)); - - // User cannot see these projects - indexForUser(USER2, - // doc with ncloc>=10K and ncloc<100K - newDoc(NCLOC, 11_000d), - // doc with ncloc>=100K and ncloc<500K - newDoc(NCLOC, 499_000d), - // doc with ncloc>= 500K - newDoc(NCLOC, 501_000d)); - - userSession.logIn(USER1); - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); - - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 3L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 0L), - entry("100000.0-500000.0", 0L), - entry("500000.0-*", 0L)); - } - - @Test - public void facet_new_lines() { - index( - // 3 docs with ncloc<1K - newDoc(NEW_LINES, 0d), - newDoc(NEW_LINES, 0d), - newDoc(NEW_LINES, 999d), - // 2 docs with ncloc>=1K and ncloc<10K - newDoc(NEW_LINES, 1_000d), - newDoc(NEW_LINES, 9_999d), - // 4 docs with ncloc>=10K and ncloc<100K - newDoc(NEW_LINES, 10_000d), - newDoc(NEW_LINES, 10_000d), - newDoc(NEW_LINES, 11_000d), - newDoc(NEW_LINES, 99_000d), - // 2 docs with ncloc>=100K and ncloc<500K - newDoc(NEW_LINES, 100_000d), - newDoc(NEW_LINES, 499_000d), - // 5 docs with ncloc>= 500K - newDoc(NEW_LINES, 500_000d), - newDoc(NEW_LINES, 100_000_000d), - newDoc(NEW_LINES, 500_000d), - newDoc(NEW_LINES, 1_000_000d), - newDoc(NEW_LINES, 100_000_000_000d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_LINES)).getFacets(); - - assertThat(facets.get(NEW_LINES)).containsExactly( - entry("*-1000.0", 3L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 4L), - entry("100000.0-500000.0", 2L), - entry("500000.0-*", 5L)); - } - - @Test - public void facet_coverage() { - index( - // 1 doc with no coverage - newDocWithNoMeasure(), - // 3 docs with coverage<30% - newDoc(COVERAGE, 0d), - newDoc(COVERAGE, 0d), - newDoc(COVERAGE, 29d), - // 2 docs with coverage>=30% and coverage<50% - newDoc(COVERAGE, 30d), - newDoc(COVERAGE, 49d), - // 4 docs with coverage>=50% and coverage<70% - newDoc(COVERAGE, 50d), - newDoc(COVERAGE, 60d), - newDoc(COVERAGE, 60d), - newDoc(COVERAGE, 69d), - // 2 docs with coverage>=70% and coverage<80% - newDoc(COVERAGE, 70d), - newDoc(COVERAGE, 79d), - // 5 docs with coverage>= 80% - newDoc(COVERAGE, 80d), - newDoc(COVERAGE, 80d), - newDoc(COVERAGE, 90d), - newDoc(COVERAGE, 90.5d), - newDoc(COVERAGE, 100d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); - - assertThat(facets.get(COVERAGE)).containsOnly( - entry("NO_DATA", 1L), - entry("*-30.0", 3L), - entry("30.0-50.0", 2L), - entry("50.0-70.0", 4L), - entry("70.0-80.0", 2L), - entry("80.0-*", 5L)); - } - - @Test - public void facet_coverage_is_sticky() { - index( - // docs with no coverage - newDoc(NCLOC, 999d, DUPLICATION, 0d), - newDoc(NCLOC, 999d, DUPLICATION, 1d), - newDoc(NCLOC, 999d, DUPLICATION, 20d), - // docs with coverage<30% - newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), - newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), - newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), - // docs with coverage>=30% and coverage<50% - newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), - newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), - // docs with coverage>=50% and coverage<70% - newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), - // docs with coverage>=70% and coverage<80% - newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), - // docs with coverage>= 80% - newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 15d), - newDoc(NCLOC, 501_000d, COVERAGE, 810d, DUPLICATION, 20d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)) - .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), - new SearchOptions().addFacets(COVERAGE, NCLOC)).getFacets(); - - // Sticky facet on coverage does not take into account coverage filter - assertThat(facets.get(COVERAGE)).containsExactly( - entry("NO_DATA", 2L), - entry("*-30.0", 3L), - entry("30.0-50.0", 2L), - entry("50.0-70.0", 1L), - entry("70.0-80.0", 1L), - entry("80.0-*", 0L)); - // But facet on ncloc does well take into into filters - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 1L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 0L), - entry("100000.0-500000.0", 0L), - entry("500000.0-*", 0L)); - } - - @Test - public void facet_coverage_contains_only_projects_authorized_for_user() { - // User can see these projects - indexForUser(USER1, - // 1 doc with no coverage - newDocWithNoMeasure(), - // docs with coverage<30% - newDoc(COVERAGE, 0d), - newDoc(COVERAGE, 0d), - newDoc(COVERAGE, 29d), - // docs with coverage>=30% and coverage<50% - newDoc(COVERAGE, 30d), - newDoc(COVERAGE, 49d)); - - // User cannot see these projects - indexForUser(USER2, - // 2 docs with no coverage - newDocWithNoMeasure(), - newDocWithNoMeasure(), - // docs with coverage>=50% and coverage<70% - newDoc(COVERAGE, 50d), - // docs with coverage>=70% and coverage<80% - newDoc(COVERAGE, 70d), - // docs with coverage>= 80% - newDoc(COVERAGE, 80d)); - - userSession.logIn(USER1); - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); - - assertThat(facets.get(COVERAGE)).containsExactly( - entry("NO_DATA", 1L), - entry("*-30.0", 3L), - entry("30.0-50.0", 2L), - entry("50.0-70.0", 0L), - entry("70.0-80.0", 0L), - entry("80.0-*", 0L)); - } - - @Test - public void facet_new_coverage() { - index( - // 1 doc with no coverage - newDocWithNoMeasure(), - // 3 docs with coverage<30% - newDoc(NEW_COVERAGE, 0d), - newDoc(NEW_COVERAGE, 0d), - newDoc(NEW_COVERAGE, 29d), - // 2 docs with coverage>=30% and coverage<50% - newDoc(NEW_COVERAGE, 30d), - newDoc(NEW_COVERAGE, 49d), - // 4 docs with coverage>=50% and coverage<70% - newDoc(NEW_COVERAGE, 50d), - newDoc(NEW_COVERAGE, 60d), - newDoc(NEW_COVERAGE, 60d), - newDoc(NEW_COVERAGE, 69d), - // 2 docs with coverage>=70% and coverage<80% - newDoc(NEW_COVERAGE, 70d), - newDoc(NEW_COVERAGE, 79d), - // 5 docs with coverage>= 80% - newDoc(NEW_COVERAGE, 80d), - newDoc(NEW_COVERAGE, 80d), - newDoc(NEW_COVERAGE, 90d), - newDoc(NEW_COVERAGE, 90.5d), - newDoc(NEW_COVERAGE, 100d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_COVERAGE)).getFacets(); - - assertThat(facets.get(NEW_COVERAGE)).containsOnly( - entry("NO_DATA", 1L), - entry("*-30.0", 3L), - entry("30.0-50.0", 2L), - entry("50.0-70.0", 4L), - entry("70.0-80.0", 2L), - entry("80.0-*", 5L)); - } - - @Test - public void facet_duplicated_lines_density() { - index( - // 1 doc with no duplication - newDocWithNoMeasure(), - // 3 docs with duplication<3% - newDoc(DUPLICATION, 0d), - newDoc(DUPLICATION, 0d), - newDoc(DUPLICATION, 2.9d), - // 2 docs with duplication>=3% and duplication<5% - newDoc(DUPLICATION, 3d), - newDoc(DUPLICATION, 4.9d), - // 4 docs with duplication>=5% and duplication<10% - newDoc(DUPLICATION, 5d), - newDoc(DUPLICATION, 6d), - newDoc(DUPLICATION, 6d), - newDoc(DUPLICATION, 9.9d), - // 2 docs with duplication>=10% and duplication<20% - newDoc(DUPLICATION, 10d), - newDoc(DUPLICATION, 19.9d), - // 5 docs with duplication>= 20% - newDoc(DUPLICATION, 20d), - newDoc(DUPLICATION, 20d), - newDoc(DUPLICATION, 50d), - newDoc(DUPLICATION, 80d), - newDoc(DUPLICATION, 100d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); - - assertThat(facets.get(DUPLICATION)).containsOnly( - entry("NO_DATA", 1L), - entry("*-3.0", 3L), - entry("3.0-5.0", 2L), - entry("5.0-10.0", 4L), - entry("10.0-20.0", 2L), - entry("20.0-*", 5L)); - } - - @Test - public void facet_duplicated_lines_density_is_sticky() { - index( - // docs with no duplication - newDoc(NCLOC, 50_001d, COVERAGE, 29d), - // docs with duplication<3% - newDoc(DUPLICATION, 0d, NCLOC, 999d, COVERAGE, 0d), - // docs with duplication>=3% and duplication<5% - newDoc(DUPLICATION, 3d, NCLOC, 5000d, COVERAGE, 0d), - newDoc(DUPLICATION, 4.9d, NCLOC, 6000d, COVERAGE, 0d), - // docs with duplication>=5% and duplication<10% - newDoc(DUPLICATION, 5d, NCLOC, 11000d, COVERAGE, 0d), - // docs with duplication>=10% and duplication<20% - newDoc(DUPLICATION, 10d, NCLOC, 120000d, COVERAGE, 10d), - newDoc(DUPLICATION, 19.9d, NCLOC, 130000d, COVERAGE, 20d), - // docs with duplication>= 20% - newDoc(DUPLICATION, 20d, NCLOC, 1000000d, COVERAGE, 40d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)) - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), - new SearchOptions().addFacets(DUPLICATION, NCLOC)).getFacets(); - - // Sticky facet on duplication does not take into account duplication filter - assertThat(facets.get(DUPLICATION)).containsOnly( - entry("NO_DATA", 1L), - entry("*-3.0", 1L), - entry("3.0-5.0", 2L), - entry("5.0-10.0", 1L), - entry("10.0-20.0", 2L), - entry("20.0-*", 0L)); - // But facet on ncloc does well take into into filters - assertThat(facets.get(NCLOC)).containsOnly( - entry("*-1000.0", 1L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 1L), - entry("100000.0-500000.0", 0L), - entry("500000.0-*", 0L)); - } - - @Test - public void facet_duplicated_lines_density_contains_only_projects_authorized_for_user() { - // User can see these projects - indexForUser(USER1, - // docs with no duplication - newDocWithNoMeasure(), - // docs with duplication<3% - newDoc(DUPLICATION, 0d), - newDoc(DUPLICATION, 0d), - newDoc(DUPLICATION, 2.9d), - // docs with duplication>=3% and duplication<5% - newDoc(DUPLICATION, 3d), - newDoc(DUPLICATION, 4.9d)); - - // User cannot see these projects - indexForUser(USER2, - // docs with no duplication - newDocWithNoMeasure(), - newDocWithNoMeasure(), - // docs with duplication>=5% and duplication<10% - newDoc(DUPLICATION, 5d), - // docs with duplication>=10% and duplication<20% - newDoc(DUPLICATION, 10d), - // docs with duplication>= 20% - newDoc(DUPLICATION, 20d)); - - userSession.logIn(USER1); - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); - - assertThat(facets.get(DUPLICATION)).containsOnly( - entry("NO_DATA", 1L), - entry("*-3.0", 3L), - entry("3.0-5.0", 2L), - entry("5.0-10.0", 0L), - entry("10.0-20.0", 0L), - entry("20.0-*", 0L)); - } - - @Test - public void facet_new_duplicated_lines_density() { - index( - // 2 docs with no measure - newDocWithNoMeasure(), - newDocWithNoMeasure(), - // 3 docs with duplication<3% - newDoc(NEW_DUPLICATION, 0d), - newDoc(NEW_DUPLICATION, 0d), - newDoc(NEW_DUPLICATION, 2.9d), - // 2 docs with duplication>=3% and duplication<5% - newDoc(NEW_DUPLICATION, 3d), - newDoc(NEW_DUPLICATION, 4.9d), - // 4 docs with duplication>=5% and duplication<10% - newDoc(NEW_DUPLICATION, 5d), - newDoc(NEW_DUPLICATION, 6d), - newDoc(NEW_DUPLICATION, 6d), - newDoc(NEW_DUPLICATION, 9.9d), - // 2 docs with duplication>=10% and duplication<20% - newDoc(NEW_DUPLICATION, 10d), - newDoc(NEW_DUPLICATION, 19.9d), - // 5 docs with duplication>= 20% - newDoc(NEW_DUPLICATION, 20d), - newDoc(NEW_DUPLICATION, 20d), - newDoc(NEW_DUPLICATION, 50d), - newDoc(NEW_DUPLICATION, 80d), - newDoc(NEW_DUPLICATION, 100d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_DUPLICATION)).getFacets(); - - assertThat(facets.get(NEW_DUPLICATION)).containsExactly( - entry("NO_DATA", 2L), - entry("*-3.0", 3L), - entry("3.0-5.0", 2L), - entry("5.0-10.0", 4L), - entry("10.0-20.0", 2L), - entry("20.0-*", 5L)); - } - - @Test - @UseDataProvider("rating_metric_keys") - public void facet_on_rating(String metricKey) { - index( - // 3 docs with rating A - newDoc(metricKey, 1d), - newDoc(metricKey, 1d), - newDoc(metricKey, 1d), - // 2 docs with rating B - newDoc(metricKey, 2d), - newDoc(metricKey, 2d), - // 4 docs with rating C - newDoc(metricKey, 3d), - newDoc(metricKey, 3d), - newDoc(metricKey, 3d), - newDoc(metricKey, 3d), - // 2 docs with rating D - newDoc(metricKey, 4d), - newDoc(metricKey, 4d), - // 5 docs with rating E - newDoc(metricKey, 5d), - newDoc(metricKey, 5d), - newDoc(metricKey, 5d), - newDoc(metricKey, 5d), - newDoc(metricKey, 5d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); - - assertThat(facets.get(metricKey)).containsExactly( - entry("1", 3L), - entry("2", 2L), - entry("3", 4L), - entry("4", 2L), - entry("5", 5L)); - } - - @Test - @UseDataProvider("rating_metric_keys") - public void facet_on_rating_is_sticky(String metricKey) { - index( - // docs with rating A - newDoc(metricKey, 1d, NCLOC, 100d, COVERAGE, 0d), - newDoc(metricKey, 1d, NCLOC, 200d, COVERAGE, 0d), - newDoc(metricKey, 1d, NCLOC, 999d, COVERAGE, 0d), - // docs with rating B - newDoc(metricKey, 2d, NCLOC, 2000d, COVERAGE, 0d), - newDoc(metricKey, 2d, NCLOC, 5000d, COVERAGE, 0d), - // docs with rating C - newDoc(metricKey, 3d, NCLOC, 20000d, COVERAGE, 0d), - newDoc(metricKey, 3d, NCLOC, 30000d, COVERAGE, 0d), - newDoc(metricKey, 3d, NCLOC, 40000d, COVERAGE, 0d), - newDoc(metricKey, 3d, NCLOC, 50000d, COVERAGE, 0d), - // docs with rating D - newDoc(metricKey, 4d, NCLOC, 120000d, COVERAGE, 0d), - // docs with rating E - newDoc(metricKey, 5d, NCLOC, 600000d, COVERAGE, 40d), - newDoc(metricKey, 5d, NCLOC, 700000d, COVERAGE, 50d), - newDoc(metricKey, 5d, NCLOC, 800000d, COVERAGE, 60d)); - - Facets facets = underTest.search(new ProjectMeasuresQuery() - .addMetricCriterion(MetricCriterion.create(metricKey, Operator.LT, 3d)) - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), - new SearchOptions().addFacets(metricKey, NCLOC)).getFacets(); - - // Sticky facet on maintainability rating does not take into account maintainability rating filter - assertThat(facets.get(metricKey)).containsExactly( - entry("1", 3L), - entry("2", 2L), - entry("3", 4L), - entry("4", 1L), - entry("5", 0L)); - // But facet on ncloc does well take into into filters - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 3L), - entry("1000.0-10000.0", 2L), - entry("10000.0-100000.0", 0L), - entry("100000.0-500000.0", 0L), - entry("500000.0-*", 0L)); - } - - @Test - @UseDataProvider("rating_metric_keys") - public void facet_on_rating_contains_only_projects_authorized_for_user(String metricKey) { - // User can see these projects - indexForUser(USER1, - // 3 docs with rating A - newDoc(metricKey, 1d), - newDoc(metricKey, 1d), - newDoc(metricKey, 1d), - // 2 docs with rating B - newDoc(metricKey, 2d), - newDoc(metricKey, 2d)); - - // User cannot see these projects - indexForUser(USER2, - // docs with rating C - newDoc(metricKey, 3d), - // docs with rating D - newDoc(metricKey, 4d), - // docs with rating E - newDoc(metricKey, 5d)); - - userSession.logIn(USER1); - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); - - assertThat(facets.get(metricKey)).containsExactly( - entry("1", 3L), - entry("2", 2L), - entry("3", 0L), - entry("4", 0L), - entry("5", 0L)); - } - - @Test - public void facet_quality_gate() { - index( - // 2 docs with QG OK - newDoc().setQualityGateStatus(OK.name()), - newDoc().setQualityGateStatus(OK.name()), - // 3 docs with QG WARN - newDoc().setQualityGateStatus(WARN.name()), - newDoc().setQualityGateStatus(WARN.name()), - newDoc().setQualityGateStatus(WARN.name()), - // 4 docs with QG ERROR - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name())); - - LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); - - assertThat(result).containsOnly( - entry(ERROR.name(), 4L), - entry(WARN.name(), 3L), - entry(OK.name(), 2L)); - } - - @Test - public void facet_quality_gate_is_sticky() { - index( - // 2 docs with QG OK - newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), - newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), - // 3 docs with QG WARN - newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), - newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), - newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), - // 4 docs with QG ERROR - newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(ERROR.name()), - newDoc(NCLOC, 5000d, COVERAGE, 40d).setQualityGateStatus(ERROR.name()), - newDoc(NCLOC, 12000d, COVERAGE, 50d).setQualityGateStatus(ERROR.name()), - newDoc(NCLOC, 13000d, COVERAGE, 60d).setQualityGateStatus(ERROR.name())); - - Facets facets = underTest.search(new ProjectMeasuresQuery() - .setQualityGateStatus(ERROR) - .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 55d)), - new SearchOptions().addFacets(ALERT_STATUS_KEY, NCLOC)).getFacets(); - - // Sticky facet on quality gate does not take into account quality gate filter - assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( - entry(OK.name(), 2L), - entry(WARN.name(), 3L), - entry(ERROR.name(), 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_quality_gate_contains_only_projects_authorized_for_user() { - // User can see these projects - indexForUser(USER1, - // 2 docs with QG OK - newDoc().setQualityGateStatus(OK.name()), - newDoc().setQualityGateStatus(OK.name()), - // 3 docs with QG WARN - newDoc().setQualityGateStatus(WARN.name()), - newDoc().setQualityGateStatus(WARN.name()), - newDoc().setQualityGateStatus(WARN.name())); - - // User cannot see these projects - indexForUser(USER2, - // 4 docs with QG ERROR - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name()), - newDoc().setQualityGateStatus(ERROR.name())); - - userSession.logIn(USER1); - LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); - - assertThat(result).containsOnly( - entry(ERROR.name(), 0L), - entry(WARN.name(), 3L), - entry(OK.name(), 2L)); - } - - @Test - public void facet_languages() { - index( - newDoc().setLanguages(singletonList("java")), - newDoc().setLanguages(singletonList("java")), - newDoc().setLanguages(singletonList("xoo")), - newDoc().setLanguages(singletonList("xml")), - newDoc().setLanguages(asList("", "java")), - newDoc().setLanguages(asList("", "java", "xoo"))); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); - - assertThat(facets.get(LANGUAGES)).containsOnly( - entry("", 2L), - entry("java", 4L), - entry("xoo", 2L), - entry("xml", 1L)); - } - - @Test - public void facet_languages_is_limited_to_10_languages() { - index( - newDoc().setLanguages(asList("", "java", "xoo", "css", "cpp")), - newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), - newDoc().setLanguages(asList("js", "scala"))); - - Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); - - assertThat(facets.get(LANGUAGES)).hasSize(10); - } - - @Test - public void facet_languages_is_sticky() { - index( - newDoc(NCLOC, 10d).setLanguages(singletonList("java")), - newDoc(NCLOC, 10d).setLanguages(singletonList("java")), - newDoc(NCLOC, 10d).setLanguages(singletonList("xoo")), - newDoc(NCLOC, 100d).setLanguages(singletonList("xml")), - newDoc(NCLOC, 100d).setLanguages(asList("", "java")), - newDoc(NCLOC, 5000d).setLanguages(asList("", "java", "xoo"))); - - Facets facets = underTest.search( - new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("java")), - new SearchOptions().addFacets(LANGUAGES, NCLOC)).getFacets(); - - // Sticky facet on language does not take into account language filter - assertThat(facets.get(LANGUAGES)).containsOnly( - entry("", 2L), - entry("java", 4L), - entry("xoo", 2L), - entry("xml", 1L)); - // But facet on ncloc does well take account into filters - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", 3L), - entry("1000.0-10000.0", 1L), - entry("10000.0-100000.0", 0L), - entry("100000.0-500000.0", 0L), - entry("500000.0-*", 0L)); - } - - @Test - public void facet_languages_returns_more_than_10_languages_when_languages_filter_contains_value_not_in_top_10() { - index( - newDoc().setLanguages(asList("", "java", "xoo", "css", "cpp")), - newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), - newDoc().setLanguages(asList("js", "scala"))); - - Facets facets = underTest.search(new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("xoo", "xml")), new SearchOptions().addFacets(LANGUAGES)).getFacets(); - - assertThat(facets.get(LANGUAGES)).containsOnly( - entry("", 1L), - entry("cpp", 1L), - entry("css", 1L), - entry("java", 1L), - entry("js", 1L), - entry("perl", 1L), - entry("php", 1L), - entry("python", 1L), - entry("ruby", 1L), - entry("scala", 1L), - entry("xoo", 1L), - entry("xml", 1L)); - } - - @Test - public void facet_languages_contains_only_projects_authorized_for_user() { - // User can see these projects - indexForUser(USER1, - newDoc().setLanguages(singletonList("java")), - newDoc().setLanguages(asList("java", "xoo"))); - - // User cannot see these projects - indexForUser(USER2, - newDoc().setLanguages(singletonList("java")), - newDoc().setLanguages(asList("java", "xoo"))); - - userSession.logIn(USER1); - LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets().get(LANGUAGES); - - assertThat(result).containsOnly( - entry("java", 2L), - entry("xoo", 1L)); - } - - @Test - public void facet_tags() { - index( - newDoc().setTags(newArrayList("finance", "offshore", "java")), - newDoc().setTags(newArrayList("finance", "javascript")), - newDoc().setTags(newArrayList("marketing", "finance")), - newDoc().setTags(newArrayList("marketing", "offshore")), - newDoc().setTags(newArrayList("finance", "marketing")), - newDoc().setTags(newArrayList("finance"))); - - Map result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); - - assertThat(result).containsOnly( - entry("finance", 5L), - entry("marketing", 3L), - entry("offshore", 2L), - entry("java", 1L), - entry("javascript", 1L)); - } - - @Test - public void facet_tags_is_sticky() { - index( - newDoc().setTags(newArrayList("finance")).setQualityGateStatus(OK.name()), - newDoc().setTags(newArrayList("finance")).setQualityGateStatus(ERROR.name()), - newDoc().setTags(newArrayList("cpp")).setQualityGateStatus(WARN.name())); - - Facets facets = underTest.search( - new ProjectMeasuresQuery().setTags(newHashSet("cpp")), - new SearchOptions().addFacets(FIELD_TAGS).addFacets(ALERT_STATUS_KEY)) - .getFacets(); - - assertThat(facets.get(FIELD_TAGS)).containsOnly( - entry("finance", 2L), - entry("cpp", 1L)); - assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( - entry(OK.name(), 0L), - entry(ERROR.name(), 0L), - entry(WARN.name(), 1L)); - } - - @Test - public void facet_tags_returns_10_elements_by_default() { - index( - newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), - newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), - newDoc().setTags(newArrayList("solo"))); - - Map result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); - - assertThat(result).hasSize(10).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10"); - } - - @Test - public void facet_tags_returns_more_than_10_tags_when_tags_filter_contains_value_not_in_top_10() { - index( - newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), - newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), - newDoc().setTags(newArrayList("solo", "solo2"))); - - Map result = underTest.search(new ProjectMeasuresQuery().setTags(ImmutableSet.of("solo", "solo2")), new SearchOptions().addFacets(FIELD_TAGS)).getFacets() - .get(FIELD_TAGS); - - assertThat(result).hasSize(12).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10", "solo", - "solo2"); - } - - @Test - public void search_tags() { - index( - newDoc().setTags(newArrayList("finance", "offshore", "java")), - newDoc().setTags(newArrayList("official", "javascript")), - newDoc().setTags(newArrayList("marketing", "official")), - newDoc().setTags(newArrayList("marketing", "Madhoff")), - newDoc().setTags(newArrayList("finance", "offshore")), - newDoc().setTags(newArrayList("offshore"))); - - List result = underTest.searchTags("off", 10); - - assertThat(result).containsOnly("offshore", "official", "Madhoff"); - } - - @Test - public void search_tags_return_all_tags() { - index( - newDoc().setTags(newArrayList("finance", "offshore", "java")), - newDoc().setTags(newArrayList("official", "javascript")), - newDoc().setTags(newArrayList("marketing", "official")), - newDoc().setTags(newArrayList("marketing", "Madhoff")), - newDoc().setTags(newArrayList("finance", "offshore")), - newDoc().setTags(newArrayList("offshore"))); - - List result = underTest.searchTags(null, 10); - - assertThat(result).containsOnly("offshore", "official", "Madhoff", "finance", "marketing", "java", "javascript"); - } - - @Test - public void search_tags_in_lexical_order() { - index( - newDoc().setTags(newArrayList("finance", "offshore", "java")), - newDoc().setTags(newArrayList("official", "javascript")), - newDoc().setTags(newArrayList("marketing", "official")), - newDoc().setTags(newArrayList("marketing", "Madhoff")), - newDoc().setTags(newArrayList("finance", "offshore")), - newDoc().setTags(newArrayList("offshore"))); - - List result = underTest.searchTags(null, 10); - - assertThat(result).containsExactly("Madhoff", "finance", "java", "javascript", "marketing", "official", "offshore"); - } - - @Test - public void search_tags_only_of_authorized_projects() { - indexForUser(USER1, - newDoc(PROJECT1).setTags(singletonList("finance")), - newDoc(PROJECT2).setTags(singletonList("marketing"))); - indexForUser(USER2, - newDoc(PROJECT3).setTags(singletonList("offshore"))); - - userSession.logIn(USER1); - - List result = underTest.searchTags(null, 10); - - assertThat(result).containsOnly("finance", "marketing"); - } - - @Test - public void search_tags_with_no_tags() { - List result = underTest.searchTags("whatever", 10); - - assertThat(result).isEmpty(); - } - - @Test - public void search_tags_with_page_size_at_0() { - index(newDoc().setTags(newArrayList("offshore"))); - - List result = underTest.searchTags(null, 0); - - assertThat(result).isEmpty(); - } - - @Test - public void search_statistics() { - es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, - newDoc("lines", 10, "coverage", 80) - .setLanguages(Arrays.asList("java", "cs", "js")) - .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 200, "cs", 250, "js", 50)), - newDoc("lines", 20, "coverage", 80) - .setLanguages(Arrays.asList("java", "python", "kotlin")) - .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 300, "python", 100, "kotlin", 404))); - - ProjectMeasuresStatistics result = underTest.searchTelemetryStatistics(); - - assertThat(result.getProjectCount()).isEqualTo(2); - assertThat(result.getProjectCountByLanguage()).containsOnly( - entry("java", 2L), entry("cs", 1L), entry("js", 1L), entry("python", 1L), entry("kotlin", 1L)); - assertThat(result.getNclocByLanguage()).containsOnly( - entry("java", 500L), entry("cs", 250L), entry("js", 50L), entry("python", 100L), entry("kotlin", 404L)); - } - - @Test - public void fail_if_page_size_greater_than_500() { - expectedException.expect(IllegalArgumentException.class); - expectedException.expectMessage("Page size must be lower than or equals to 500"); - - underTest.searchTags("whatever", 501); - } - - private void index(ProjectMeasuresDoc... docs) { - es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); - for (ProjectMeasuresDoc doc : docs) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), Qualifiers.PROJECT); - access.allowAnyone(); - authorizationIndexerTester.allow(access); - } - } - - private void indexForUser(UserDto user, ProjectMeasuresDoc... docs) { - es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); - for (ProjectMeasuresDoc doc : docs) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), Qualifiers.PROJECT); - access.addUserId(user.getId()); - authorizationIndexerTester.allow(access); - } - } - - private void indexForGroup(GroupDto group, ProjectMeasuresDoc... docs) { - es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); - for (ProjectMeasuresDoc doc : docs) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), Qualifiers.PROJECT); - access.addGroupId(group.getId()); - authorizationIndexerTester.allow(access); - } - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project) { - return new ProjectMeasuresDoc() - .setOrganizationUuid(project.getOrganizationUuid()) - .setId(project.uuid()) - .setKey(project.getDbKey()) - .setName(project.name()); - } - - private static ProjectMeasuresDoc newDoc() { - return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { - return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2) { - return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2))); - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { - return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2), newMeasure(metric3, value3))); - } - - private static Map newMeasure(String key, Object value) { - return ImmutableMap.of("key", key, "value", value); - } - - private static ProjectMeasuresDoc newDocWithNoMeasure() { - return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); - } - - private static ProjectMeasuresDoc newDoc(String metric1, Object value1) { - return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1); - } - - private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2) { - return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2); - } - - private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { - return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2, metric3, value3); - } - - private void assertResults(ProjectMeasuresQuery query, ComponentDto... expectedProjects) { - List result = underTest.search(query, new SearchOptions()).getIds(); - assertThat(result).containsExactly(Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new)); - } - - private void assertNoResults(ProjectMeasuresQuery query) { - assertResults(query); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java deleted file mode 100644 index d66998b3ab7..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import com.google.common.collect.ImmutableMap; -import java.util.List; -import java.util.Map; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.resources.Qualifiers; -import org.sonar.api.utils.System2; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.organization.OrganizationTesting; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.Facets; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; -import org.sonar.server.permission.index.PermissionIndexerTester; -import org.sonar.server.user.LightUserSessionRule; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.collect.Lists.newArrayList; -import static java.util.Arrays.stream; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; -import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES; -import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.GT; -import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LT; - -public class ProjectMeasuresIndexTextSearchTest { - - private static final String NCLOC = "ncloc"; - - private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); - - @Rule - public EsTester es = EsTester.create(); - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - - private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); - private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); - private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); - - @Test - public void match_exact_case_insensitive_name() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")), - newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube"))); - - assertTextQueryResults("Apache Struts", "struts"); - assertTextQueryResults("APACHE STRUTS", "struts"); - assertTextQueryResults("APACHE struTS", "struts"); - } - - @Test - public void match_from_sub_name() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); - - assertTextQueryResults("truts", "struts"); - assertTextQueryResults("pache", "struts"); - assertTextQueryResults("apach", "struts"); - assertTextQueryResults("che stru", "struts"); - } - - @Test - public void match_name_with_dot() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache.Struts"))); - - assertTextQueryResults("apache struts", "struts"); - } - - @Test - public void match_partial_name() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("XstrutsxXjavax"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_partial_name_prefix_word1() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.java"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_partial_name_suffix_word1() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("StrutsObject.java"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_partial_name_prefix_word2() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.xjava"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_partial_name_suffix_word2() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStrutsObject.xjavax"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_subset_of_document_terms() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Some.Struts.Project.java.old"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void match_partial_match_prefix_and_suffix_everywhere() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.javax"))); - - assertTextQueryResults("struts java", "struts"); - } - - @Test - public void ignore_empty_words() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Struts"))); - - assertTextQueryResults(" struts \n \n\n", "struts"); - } - - @Test - public void match_name_from_prefix() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); - - assertTextQueryResults("apach", "struts"); - assertTextQueryResults("ApA", "struts"); - assertTextQueryResults("AP", "struts"); - } - - @Test - public void match_name_from_two_words() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("ApacheStrutsFoundation"))); - - assertTextQueryResults("apache struts", "project"); - assertTextQueryResults("struts apache", "project"); - // Only one word is matching - assertNoResults("apache plugin"); - assertNoResults("project struts"); - } - - @Test - public void match_long_name() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("LongNameLongNameLongNameLongNameSonarQube")), - newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("LongNameLongNameLongNameLongNameSonarQubeX"))); - - assertTextQueryResults("LongNameLongNameLongNameLongNameSonarQube", "project1", "project2"); - } - - @Test - public void match_name_with_two_characters() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); - - assertTextQueryResults("st", "struts"); - assertTextQueryResults("tr", "struts"); - } - - @Test - public void match_exact_case_insensitive_key() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1")), - newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"))); - - assertTextQueryResults("project1", "project1"); - assertTextQueryResults("PROJECT1", "project1"); - assertTextQueryResults("pRoJecT1", "project1"); - } - - @Test - public void match_key_with_dot() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube")), - newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); - - assertTextQueryResults("org.sonarqube", "sonarqube"); - assertNoResults("orgsonarqube"); - assertNoResults("org-sonarqube"); - assertNoResults("org:sonarqube"); - assertNoResults("org sonarqube"); - } - - @Test - public void match_key_with_dash() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org-sonarqube")), - newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); - - assertTextQueryResults("org-sonarqube", "sonarqube"); - assertNoResults("orgsonarqube"); - assertNoResults("org.sonarqube"); - assertNoResults("org:sonarqube"); - assertNoResults("org sonarqube"); - } - - @Test - public void match_key_with_colon() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org:sonarqube")), - newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); - - assertTextQueryResults("org:sonarqube", "sonarqube"); - assertNoResults("orgsonarqube"); - assertNoResults("org-sonarqube"); - assertNoResults("org_sonarqube"); - assertNoResults("org sonarqube"); - } - - @Test - public void match_key_having_all_special_characters() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube:sonar-sérvèr_ç"))); - - assertTextQueryResults("org.sonarqube:sonar-sérvèr_ç", "sonarqube"); - } - - @Test - public void does_not_match_partial_key() { - index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("some name").setDbKey("theKey"))); - - assertNoResults("theke"); - assertNoResults("hekey"); - } - - @Test - public void facets_take_into_account_text_search() { - index( - // docs with ncloc<1K - newDoc(newPrivateProjectDto(ORG).setName("Windows").setDbKey("project1"), NCLOC, 0d), - newDoc(newPrivateProjectDto(ORG).setName("apachee").setDbKey("project2"), NCLOC, 999d), - // docs with ncloc>=1K and ncloc<10K - newDoc(newPrivateProjectDto(ORG).setName("Apache").setDbKey("project3"), NCLOC, 1_000d), - // docs with ncloc>=100K and ncloc<500K - newDoc(newPrivateProjectDto(ORG).setName("Apache Foundation").setDbKey("project4"), NCLOC, 100_000d)); - - assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache"), 1L, 1L, 0L, 1L, 0L); - assertNclocFacet(new ProjectMeasuresQuery().setQueryText("PAch"), 1L, 1L, 0L, 1L, 0L); - assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache foundation"), 0L, 0L, 0L, 1L, 0L); - assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project3"), 0L, 1L, 0L, 0L, 0L); - assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project"), 0L, 0L, 0L, 0L, 0L); - } - - @Test - public void filter_by_metric_take_into_account_text_search() { - index( - newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1"), NCLOC, 30_000d), - newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"), NCLOC, 40_000d), - newDoc(newPrivateProjectDto(ORG).setUuid("project3").setName("Apache").setDbKey("project3"), NCLOC, 50_000d), - newDoc(newPrivateProjectDto(ORG).setUuid("project4").setName("Apache").setDbKey("project4"), NCLOC, 60_000d)); - - assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d)), "project3", "project4", "project2"); - assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); - assertResults(new ProjectMeasuresQuery().setQueryText("PAC").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); - assertResults(new ProjectMeasuresQuery().setQueryText("apachee").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 30_000d)), "project2"); - assertResults(new ProjectMeasuresQuery().setQueryText("unknown").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d))); - } - - private void index(ProjectMeasuresDoc... docs) { - es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); - stream(docs).forEach(doc -> { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), Qualifiers.PROJECT); - access.allowAnyone(); - authorizationIndexerTester.allow(access); - }); - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project) { - return new ProjectMeasuresDoc() - .setOrganizationUuid(project.getOrganizationUuid()) - .setId(project.uuid()) - .setKey(project.getDbKey()) - .setName(project.name()); - } - - private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { - return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); - } - - private static Map newMeasure(String key, Object value) { - return ImmutableMap.of("key", key, "value", value); - } - - private void assertResults(ProjectMeasuresQuery query, String... expectedProjectUuids) { - List result = underTest.search(query, new SearchOptions()).getIds(); - assertThat(result).containsExactly(expectedProjectUuids); - } - - private void assertTextQueryResults(String queryText, String... expectedProjectUuids) { - assertResults(new ProjectMeasuresQuery().setQueryText(queryText), expectedProjectUuids); - } - - private void assertNoResults(String queryText) { - assertTextQueryResults(queryText); - } - - private void assertNclocFacet(ProjectMeasuresQuery query, Long... facetExpectedValues) { - checkArgument(facetExpectedValues.length == 5, "5 facet values is required"); - Facets facets = underTest.search(query, new SearchOptions().addFacets(NCLOC)).getFacets(); - assertThat(facets.get(NCLOC)).containsExactly( - entry("*-1000.0", facetExpectedValues[0]), - entry("1000.0-10000.0", facetExpectedValues[1]), - entry("10000.0-100000.0", facetExpectedValues[2]), - entry("100000.0-500000.0", facetExpectedValues[3]), - entry("500000.0-*", facetExpectedValues[4])); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java deleted file mode 100644 index 6da1e29c8eb..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.measures.Metric.Level; -import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.groups.Tuple.tuple; -import static org.sonar.api.measures.Metric.Level.OK; -import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ; - -public class ProjectMeasuresQueryTest { - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - - private ProjectMeasuresQuery underTest = new ProjectMeasuresQuery(); - - @Test - public void empty_query() { - assertThat(underTest.getMetricCriteria()).isEmpty(); - assertThat(underTest.getQualityGateStatus()).isEmpty(); - assertThat(underTest.getOrganizationUuid()).isEmpty(); - } - - @Test - public void add_metric_criterion() { - underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); - - assertThat(underTest.getMetricCriteria()) - .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) - .containsOnly(tuple("coverage", EQ, 10d)); - } - - @Test - public void isNoData_returns_true_when_no_data() { - underTest.addMetricCriterion(MetricCriterion.createNoData("coverage")); - - assertThat(underTest.getMetricCriteria()) - .extracting(MetricCriterion::getMetricKey, MetricCriterion::isNoData) - .containsOnly(tuple("coverage", true)); - } - - @Test - public void isNoData_returns_false_when_data_exists() { - underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); - - assertThat(underTest.getMetricCriteria()) - .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::isNoData) - .containsOnly(tuple("coverage", EQ, false)); - } - - @Test - public void set_quality_gate_status() { - underTest.setQualityGateStatus(OK); - - assertThat(underTest.getQualityGateStatus().get()).isEqualTo(Level.OK); - } - - @Test - public void default_sort_is_by_name() { - assertThat(underTest.getSort()).isEqualTo("name"); - } - - @Test - public void fail_to_set_null_sort() { - expectedException.expect(NullPointerException.class); - expectedException.expectMessage("Sort cannot be null"); - - underTest.setSort(null); - } - - @Test - public void fail_to_get_value_when_no_data() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("The criterion for metric coverage has no data"); - - MetricCriterion.createNoData("coverage").getValue(); - } - - @Test - public void fail_to_get_operator_when_no_data() { - expectedException.expect(IllegalStateException.class); - expectedException.expectMessage("The criterion for metric coverage has no data"); - - MetricCriterion.createNoData("coverage").getOperator(); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java deleted file mode 100644 index 9f365216ec7..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure.index; - -import org.junit.Test; -import org.sonar.core.platform.ComponentContainer; -import org.sonar.server.measure.index.ProjectsEsModule; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ProjectsEsModuleTest { - @Test - public void verify_count_of_added_components() { - ComponentContainer container = new ComponentContainer(); - new ProjectsEsModule().configure(container); - assertThat(container.size()).isEqualTo(3 + 2); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/AuthorizationTypeSupportTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/AuthorizationTypeSupportTest.java deleted file mode 100644 index e4d0d77ab03..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/AuthorizationTypeSupportTest.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import org.elasticsearch.index.query.MatchAllQueryBuilder; -import org.elasticsearch.index.query.QueryBuilder; -import org.elasticsearch.join.query.HasParentQueryBuilder; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.GroupTesting; -import org.sonar.server.user.LightUserSessionRule; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.test.JsonAssert.assertJson; - -public class AuthorizationTypeSupportTest { - - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - - private AuthorizationTypeSupport underTest = new AuthorizationTypeSupport(userSession); - - @Test - public void createQueryFilter_does_not_include_permission_filters_if_user_is_flagged_as_root() { - userSession.logIn().setRoot(); - - QueryBuilder filter = underTest.createQueryFilter(); - - assertThat(filter).isInstanceOf(MatchAllQueryBuilder.class); - } - - @Test - public void createQueryFilter_sets_filter_on_anyone_group_if_user_is_anonymous() { - userSession.anonymous(); - - HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); - - assertJson(filter.toString()).isSimilarTo("{" + - " \"has_parent\" : {" + - " \"query\" : {" + - " \"bool\" : {" + - " \"filter\" : [{" + - " \"bool\" : {" + - " \"should\" : [{" + - " \"term\" : {" + - " \"allowAnyone\" : {\"value\": true}" + - " }" + - " }]" + - " }" + - " }]" + - " }" + - " }," + - " \"parent_type\" : \"authorization\"" + - " }" + - "}"); - } - - @Test - public void createQueryFilter_sets_filter_on_anyone_and_user_id_if_user_is_logged_in_but_has_no_groups() { - userSession.logIn().setUserId(1234); - - HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); - - assertJson(filter.toString()).isSimilarTo("{" + - " \"has_parent\": {" + - " \"query\": {" + - " \"bool\": {" + - " \"filter\": [{" + - " \"bool\": {" + - " \"should\": [" + - " {" + - " \"term\": {" + - " \"allowAnyone\": {\"value\": true}" + - " }" + - " }," + - " {" + - " \"term\": {" + - " \"userIds\": {\"value\": 1234}" + - " }" + - " }" + - " ]" + - " }" + - " }]" + - " }" + - " }," + - " \"parent_type\": \"authorization\"" + - " }" + - "}"); - } - - @Test - public void createQueryFilter_sets_filter_on_anyone_and_user_id_and_group_ids_if_user_is_logged_in_and_has_groups() { - GroupDto group1 = GroupTesting.newGroupDto().setId(10); - GroupDto group2 = GroupTesting.newGroupDto().setId(11); - userSession.logIn().setUserId(1234).setGroups(group1, group2); - - HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); - - assertJson(filter.toString()).isSimilarTo("{" + - " \"has_parent\": {" + - " \"query\": {" + - " \"bool\": {" + - " \"filter\": [{" + - " \"bool\": {" + - " \"should\": [" + - " {" + - " \"term\": {" + - " \"allowAnyone\": {\"value\": true}" + - " }" + - " }," + - " {" + - " \"term\": {" + - " \"userIds\": {\"value\": 1234}" + - " }" + - " }," + - " {" + - " \"term\": {" + - " \"groupIds\": {\"value\": 10}" + - " }" + - " }," + - " {" + - " \"term\": {" + - " \"groupIds\": {\"value\": 11}" + - " }" + - " }" + - " ]" + - " }" + - " }]" + - " }" + - " }," + - " \"parent_type\": \"authorization\"" + - " }" + - "}"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndex.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndex.java deleted file mode 100644 index 8f0c8dcf15a..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndex.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import java.util.Arrays; -import java.util.List; -import org.elasticsearch.index.query.QueryBuilders; -import org.elasticsearch.search.SearchHits; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.server.es.EsClient; - -import static org.sonar.server.permission.index.FooIndexDefinition.FOO_INDEX; -import static org.sonar.server.permission.index.FooIndexDefinition.FOO_TYPE; - -public class FooIndex { - - private final EsClient esClient; - private final AuthorizationTypeSupport authorizationTypeSupport; - - public FooIndex(EsClient esClient, AuthorizationTypeSupport authorizationTypeSupport) { - this.esClient = esClient; - this.authorizationTypeSupport = authorizationTypeSupport; - } - - public boolean hasAccessToProject(String projectUuid) { - SearchHits hits = esClient.prepareSearch(FOO_INDEX) - .setTypes(FOO_TYPE) - .setQuery(QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) - .filter(authorizationTypeSupport.createQueryFilter())) - .get() - .getHits(); - List names = Arrays.stream(hits.hits()) - .map(h -> h.getSource().get(FooIndexDefinition.FIELD_NAME).toString()) - .collect(MoreCollectors.toList()); - return names.size() == 2 && names.contains("bar") && names.contains("baz"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java deleted file mode 100644 index fa3d294e48a..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import org.sonar.api.config.internal.MapSettings; -import org.sonar.server.es.IndexDefinition; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.NewIndex; - -import static org.sonar.server.es.NewIndex.SettingsConfiguration.MANUAL_REFRESH_INTERVAL; -import static org.sonar.server.es.NewIndex.SettingsConfiguration.newBuilder; - -public class FooIndexDefinition implements IndexDefinition { - - public static final String FOO_INDEX = "foos"; - public static final String FOO_TYPE = "foo"; - public static final IndexType INDEX_TYPE_FOO = new IndexType(FOO_INDEX, FOO_TYPE); - public static final String FIELD_NAME = "name"; - public static final String FIELD_PROJECT_UUID = "projectUuid"; - - @Override - public void define(IndexDefinitionContext context) { - NewIndex index = context.create(FOO_INDEX, newBuilder(new MapSettings().asConfig()).setRefreshInterval(MANUAL_REFRESH_INTERVAL).build()); - - NewIndex.NewIndexType type = index.createType(FOO_TYPE) - .requireProjectAuthorization(); - - type.keywordFieldBuilder(FIELD_NAME).build(); - type.keywordFieldBuilder(FIELD_PROJECT_UUID).build(); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexer.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexer.java deleted file mode 100644 index f5122681075..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/FooIndexer.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; -import java.util.Collection; -import java.util.Set; -import org.sonar.db.DbSession; -import org.sonar.db.es.EsQueueDto; -import org.sonar.server.es.EsClient; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.IndexingResult; -import org.sonar.server.es.ProjectIndexer; - -import static org.sonar.server.permission.index.FooIndexDefinition.INDEX_TYPE_FOO; - -public class FooIndexer implements ProjectIndexer, NeedAuthorizationIndexer { - - private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_FOO, p -> true); - - private final EsClient esClient; - - public FooIndexer(EsClient esClient) { - this.esClient = esClient; - } - - @Override - public AuthorizationScope getAuthorizationScope() { - return AUTHORIZATION_SCOPE; - } - - @Override - public void indexOnAnalysis(String branchUuid) { - addToIndex(branchUuid, "bar"); - addToIndex(branchUuid, "baz"); - } - - @Override - public Collection prepareForRecovery(DbSession dbSession, Collection projectUuids, Cause cause) { - throw new UnsupportedOperationException(); - } - - private void addToIndex(String projectUuid, String name) { - esClient.prepareIndex(INDEX_TYPE_FOO) - .setRouting(projectUuid) - .setParent(projectUuid) - .setSource(ImmutableMap.of( - FooIndexDefinition.FIELD_NAME, name, - FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) - .get(); - } - - @Override - public void indexOnStartup(Set uninitializedIndexTypes) { - throw new UnsupportedOperationException(); - } - - @Override - public Set getIndexTypes() { - return ImmutableSet.of(INDEX_TYPE_FOO); - } - - @Override - public IndexingResult index(DbSession dbSession, Collection items) { - throw new UnsupportedOperationException(); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java deleted file mode 100644 index 75eb48e6e53..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java +++ /dev/null @@ -1,273 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.sonar.api.utils.System2; -import org.sonar.core.util.stream.MoreCollectors; -import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.permission.GroupPermissionDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDbTester; -import org.sonar.db.user.UserDto; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.api.resources.Qualifiers.APP; -import static org.sonar.api.resources.Qualifiers.PROJECT; -import static org.sonar.api.resources.Qualifiers.VIEW; -import static org.sonar.api.web.UserRole.ADMIN; -import static org.sonar.api.web.UserRole.USER; - -public class PermissionIndexerDaoTest { - - @Rule - public DbTester dbTester = DbTester.create(System2.INSTANCE); - - private DbClient dbClient = dbTester.getDbClient(); - private DbSession dbSession = dbTester.getSession(); - - private ComponentDbTester componentDbTester = new ComponentDbTester(dbTester); - private UserDbTester userDbTester = new UserDbTester(dbTester); - - private OrganizationDto organization; - private ComponentDto publicProject; - private ComponentDto privateProject1; - private ComponentDto privateProject2; - private ComponentDto view1; - private ComponentDto view2; - private ComponentDto application; - private UserDto user1; - private UserDto user2; - private GroupDto group; - - private PermissionIndexerDao underTest = new PermissionIndexerDao(); - - @Before - public void setUp() { - organization = dbTester.organizations().insert(); - publicProject = componentDbTester.insertPublicProject(organization); - privateProject1 = componentDbTester.insertPrivateProject(organization); - privateProject2 = componentDbTester.insertPrivateProject(organization); - view1 = componentDbTester.insertView(organization); - view2 = componentDbTester.insertView(organization); - application = componentDbTester.insertApplication(organization); - user1 = userDbTester.insertUser(); - user2 = userDbTester.insertUser(); - group = userDbTester.insertGroup(organization); - } - - @Test - public void select_all() { - insertTestDataForProjectsAndViews(); - - Collection dtos = underTest.selectAll(dbClient, dbSession); - assertThat(dtos).hasSize(6); - - PermissionIndexerDao.Dto publicProjectAuthorization = getByProjectUuid(publicProject.uuid(), dtos); - isPublic(publicProjectAuthorization, PROJECT); - - PermissionIndexerDao.Dto view1Authorization = getByProjectUuid(view1.uuid(), dtos); - isPublic(view1Authorization, VIEW); - - PermissionIndexerDao.Dto applicationAuthorization = getByProjectUuid(application.uuid(), dtos); - isPublic(applicationAuthorization, APP); - - PermissionIndexerDao.Dto privateProject1Authorization = getByProjectUuid(privateProject1.uuid(), dtos); - assertThat(privateProject1Authorization.getGroupIds()).containsOnly(group.getId()); - assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject1Authorization.getUserIds()).containsOnly(user1.getId(), user2.getId()); - assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); - - PermissionIndexerDao.Dto privateProject2Authorization = getByProjectUuid(privateProject2.uuid(), dtos); - assertThat(privateProject2Authorization.getGroupIds()).isEmpty(); - assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject2Authorization.getUserIds()).containsOnly(user1.getId()); - assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); - - PermissionIndexerDao.Dto view2Authorization = getByProjectUuid(view2.uuid(), dtos); - isPublic(view2Authorization, VIEW); - } - - @Test - public void selectByUuids() { - insertTestDataForProjectsAndViews(); - - Map dtos = underTest - .selectByUuids(dbClient, dbSession, asList(publicProject.uuid(), privateProject1.uuid(), privateProject2.uuid(), view1.uuid(), view2.uuid(), application.uuid())) - .stream() - .collect(MoreCollectors.uniqueIndex(PermissionIndexerDao.Dto::getProjectUuid, Function.identity())); - assertThat(dtos).hasSize(6); - - PermissionIndexerDao.Dto publicProjectAuthorization = dtos.get(publicProject.uuid()); - isPublic(publicProjectAuthorization, PROJECT); - - PermissionIndexerDao.Dto view1Authorization = dtos.get(view1.uuid()); - isPublic(view1Authorization, VIEW); - - PermissionIndexerDao.Dto applicationAuthorization = dtos.get(application.uuid()); - isPublic(applicationAuthorization, APP); - - PermissionIndexerDao.Dto privateProject1Authorization = dtos.get(privateProject1.uuid()); - assertThat(privateProject1Authorization.getGroupIds()).containsOnly(group.getId()); - assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject1Authorization.getUserIds()).containsOnly(user1.getId(), user2.getId()); - assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); - - PermissionIndexerDao.Dto privateProject2Authorization = dtos.get(privateProject2.uuid()); - assertThat(privateProject2Authorization.getGroupIds()).isEmpty(); - assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); - assertThat(privateProject2Authorization.getUserIds()).containsOnly(user1.getId()); - assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); - - PermissionIndexerDao.Dto view2Authorization = dtos.get(view2.uuid()); - isPublic(view2Authorization, VIEW); - } - - @Test - public void selectByUuids_returns_empty_list_when_project_does_not_exist() { - insertTestDataForProjectsAndViews(); - - List dtos = underTest.selectByUuids(dbClient, dbSession, asList("missing")); - assertThat(dtos).isEmpty(); - } - - @Test - public void select_by_projects_with_high_number_of_projects() { - List projectUuids = new ArrayList<>(); - for (int i = 0; i < 350; i++) { - ComponentDto project = ComponentTesting.newPrivateProjectDto(organization, Integer.toString(i)); - dbClient.componentDao().insert(dbSession, project); - projectUuids.add(project.uuid()); - GroupPermissionDto dto = new GroupPermissionDto() - .setOrganizationUuid(group.getOrganizationUuid()) - .setGroupId(group.getId()) - .setRole(USER) - .setResourceId(project.getId()); - dbClient.groupPermissionDao().insert(dbSession, dto); - } - dbSession.commit(); - - assertThat(underTest.selectByUuids(dbClient, dbSession, projectUuids)) - .hasSize(350) - .extracting(PermissionIndexerDao.Dto::getProjectUuid) - .containsAll(projectUuids); - } - - @Test - public void return_private_project_without_any_permission_when_no_permission_in_DB() { - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); - - // no permissions - assertThat(dtos).hasSize(1); - PermissionIndexerDao.Dto dto = dtos.get(0); - assertThat(dto.getGroupIds()).isEmpty(); - assertThat(dto.getUserIds()).isEmpty(); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); - } - - @Test - public void return_public_project_with_only_AllowAnyone_true_when_no_permission_in_DB() { - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(publicProject.uuid())); - - assertThat(dtos).hasSize(1); - PermissionIndexerDao.Dto dto = dtos.get(0); - assertThat(dto.getGroupIds()).isEmpty(); - assertThat(dto.getUserIds()).isEmpty(); - assertThat(dto.isAllowAnyone()).isTrue(); - assertThat(dto.getProjectUuid()).isEqualTo(publicProject.uuid()); - assertThat(dto.getQualifier()).isEqualTo(publicProject.qualifier()); - } - - @Test - public void return_private_project_with_AllowAnyone_false_and_user_id_when_user_is_granted_USER_permission_directly() { - dbTester.users().insertProjectPermissionOnUser(user1, USER, privateProject1); - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); - - assertThat(dtos).hasSize(1); - PermissionIndexerDao.Dto dto = dtos.get(0); - assertThat(dto.getGroupIds()).isEmpty(); - assertThat(dto.getUserIds()).containsOnly(user1.getId()); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); - } - - @Test - public void return_private_project_with_AllowAnyone_false_and_group_id_but_not_user_id_when_user_is_granted_USER_permission_through_group() { - dbTester.users().insertMember(group, user1); - dbTester.users().insertProjectPermissionOnGroup(group, USER, privateProject1); - List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); - - assertThat(dtos).hasSize(1); - PermissionIndexerDao.Dto dto = dtos.get(0); - assertThat(dto.getGroupIds()).containsOnly(group.getId()); - assertThat(dto.getUserIds()).isEmpty(); - assertThat(dto.isAllowAnyone()).isFalse(); - assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); - assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); - } - - private void isPublic(PermissionIndexerDao.Dto view1Authorization, String qualifier) { - assertThat(view1Authorization.getGroupIds()).isEmpty(); - assertThat(view1Authorization.isAllowAnyone()).isTrue(); - assertThat(view1Authorization.getUserIds()).isEmpty(); - assertThat(view1Authorization.getQualifier()).isEqualTo(qualifier); - } - - private static PermissionIndexerDao.Dto getByProjectUuid(String projectUuid, Collection dtos) { - return dtos.stream().filter(dto -> dto.getProjectUuid().equals(projectUuid)).findFirst().orElseThrow(IllegalArgumentException::new); - } - - private void insertTestDataForProjectsAndViews() { - // user1 has USER access on both private projects - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, publicProject); - userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject1); - userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject2); - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, view1); - userDbTester.insertProjectPermissionOnUser(user1, ADMIN, application); - - // user2 has USER access on privateProject1 only - userDbTester.insertProjectPermissionOnUser(user2, USER, privateProject1); - userDbTester.insertProjectPermissionOnUser(user2, ADMIN, privateProject2); - - // group1 has USER access on privateProject1 only - userDbTester.insertProjectPermissionOnGroup(group, USER, privateProject1); - userDbTester.insertProjectPermissionOnGroup(group, ADMIN, privateProject1); - userDbTester.insertProjectPermissionOnGroup(group, ADMIN, view1); - userDbTester.insertProjectPermissionOnGroup(group, ADMIN, application); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java deleted file mode 100644 index cab4ce4a0ce..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java +++ /dev/null @@ -1,430 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import java.util.Collection; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.sonar.api.utils.System2; -import org.sonar.db.DbSession; -import org.sonar.db.DbTester; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.es.EsQueueDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.es.EsTester; -import org.sonar.server.es.IndexType; -import org.sonar.server.es.IndexingResult; -import org.sonar.server.es.ProjectIndexer; -import org.sonar.server.user.LightUserSessionRule; - -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.sonar.api.web.UserRole.ADMIN; -import static org.sonar.api.web.UserRole.USER; -import static org.sonar.server.es.ProjectIndexer.Cause.PERMISSION_CHANGE; - -public class PermissionIndexerTest { - - private static final IndexType INDEX_TYPE_FOO_AUTH = AuthorizationTypeSupport.getAuthorizationIndexType(FooIndexDefinition.INDEX_TYPE_FOO); - - @Rule - public ExpectedException expectedException = ExpectedException.none(); - @Rule - public DbTester db = DbTester.create(System2.INSTANCE); - @Rule - public EsTester es = EsTester.createCustom(new FooIndexDefinition()); - @Rule - public LightUserSessionRule userSession = new LightUserSessionRule(); - - private FooIndex fooIndex = new FooIndex(es.client(), new AuthorizationTypeSupport(userSession)); - private FooIndexer fooIndexer = new FooIndexer(es.client()); - private PermissionIndexer underTest = new PermissionIndexer(db.getDbClient(), es.client(), fooIndexer); - - @Test - public void indexOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { - ComponentDto project = createAndIndexPublicProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - - indexOnStartup(); - - verifyAnyoneAuthorized(project); - verifyAuthorized(project, user1); - verifyAuthorized(project, user2); - } - - @Test - public void deletion_resilience_will_deindex_projects() { - ComponentDto project1 = createUnindexedPublicProject(); - ComponentDto project2 = createUnindexedPublicProject(); - //UserDto user1 = db.users().insertUser(); - indexOnStartup(); - assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2); - - // Simulate a indexation issue - db.getDbClient().componentDao().delete(db.getSession(), project1.getId()); - underTest.prepareForRecovery(db.getSession(), asList(project1.uuid()), ProjectIndexer.Cause.PROJECT_DELETION); - assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(1); - Collection esQueueDtos = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), Long.MAX_VALUE, 2); - - underTest.index(db.getSession(), esQueueDtos); - - assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(0); - assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(1); - } - - @Test - public void indexOnStartup_grants_access_to_user() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - db.users().insertProjectPermissionOnUser(user2, ADMIN, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // user1 has access - verifyAuthorized(project, user1); - - // user2 has not access (only USER permission is accepted) - verifyNotAuthorized(project, user2); - } - - @Test - public void indexOnStartup_grants_access_to_group_on_private_project() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - UserDto user3 = db.users().insertUser(); - GroupDto group1 = db.users().insertGroup(); - GroupDto group2 = db.users().insertGroup(); - db.users().insertProjectPermissionOnGroup(group1, USER, project); - db.users().insertProjectPermissionOnGroup(group2, ADMIN, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // group1 has access - verifyAuthorized(project, user1, group1); - - // group2 has not access (only USER permission is accepted) - verifyNotAuthorized(project, user2, group2); - - // user3 is not in any group - verifyNotAuthorized(project, user3); - } - - @Test - public void indexOnStartup_grants_access_to_user_and_group() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - db.users().insertMember(group, user2); - db.users().insertProjectPermissionOnUser(user1, USER, project); - db.users().insertProjectPermissionOnGroup(group, USER, project); - - indexOnStartup(); - - // anonymous - verifyAnyoneNotAuthorized(project); - - // has direct access - verifyAuthorized(project, user1); - - // has access through group - verifyAuthorized(project, user1, group); - - // no access - verifyNotAuthorized(project, user2); - } - - @Test - public void indexOnStartup_does_not_grant_access_to_anybody_on_private_project() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneNotAuthorized(project); - verifyNotAuthorized(project, user); - verifyNotAuthorized(project, user, group); - } - - @Test - public void indexOnStartup_grants_access_to_anybody_on_public_project() { - ComponentDto project = createAndIndexPublicProject(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneAuthorized(project); - verifyAuthorized(project, user); - verifyAuthorized(project, user, group); - } - - @Test - public void indexOnStartup_grants_access_to_anybody_on_view() { - ComponentDto view = createAndIndexView(); - UserDto user = db.users().insertUser(); - GroupDto group = db.users().insertGroup(); - - indexOnStartup(); - - verifyAnyoneAuthorized(view); - verifyAuthorized(view, user); - verifyAuthorized(view, user, group); - } - - @Test - public void indexOnStartup_grants_access_on_many_projects() { - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - ComponentDto project = null; - for (int i = 0; i < 10; i++) { - project = createAndIndexPrivateProject(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - } - - indexOnStartup(); - - verifyAnyoneNotAuthorized(project); - verifyAuthorized(project, user1); - verifyNotAuthorized(project, user2); - } - - @Test - public void public_projects_are_visible_to_anybody_whatever_the_organization() { - ComponentDto projectOnOrg1 = createAndIndexPublicProject(db.organizations().insert()); - ComponentDto projectOnOrg2 = createAndIndexPublicProject(db.organizations().insert()); - UserDto user = db.users().insertUser(); - - indexOnStartup(); - - verifyAnyoneAuthorized(projectOnOrg1); - verifyAnyoneAuthorized(projectOnOrg2); - verifyAuthorized(projectOnOrg1, user); - verifyAuthorized(projectOnOrg2, user); - } - - @Test - public void indexOnAnalysis_does_nothing_because_CE_does_not_touch_permissions() { - ComponentDto project = createAndIndexPublicProject(); - - underTest.indexOnAnalysis(project.uuid()); - - assertThatAuthIndexHasSize(0); - verifyAnyoneNotAuthorized(project); - } - - @Test - public void permissions_are_not_updated_on_project_tags_update() { - ComponentDto project = createAndIndexPublicProject(); - - indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE); - - assertThatAuthIndexHasSize(0); - verifyAnyoneNotAuthorized(project); - } - - @Test - public void permissions_are_not_updated_on_project_key_update() { - ComponentDto project = createAndIndexPublicProject(); - - indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE); - - assertThatAuthIndexHasSize(0); - verifyAnyoneNotAuthorized(project); - } - - @Test - public void index_permissions_on_project_creation() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user, USER, project); - - indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); - - assertThatAuthIndexHasSize(1); - verifyAuthorized(project, user); - } - - @Test - public void index_permissions_on_permission_change() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user1 = db.users().insertUser(); - UserDto user2 = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user1, USER, project); - indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); - verifyAuthorized(project, user1); - verifyNotAuthorized(project, user2); - - db.users().insertProjectPermissionOnUser(user2, USER, project); - indexPermissions(project, PERMISSION_CHANGE); - - verifyAuthorized(project, user1); - verifyAuthorized(project, user1); - } - - @Test - public void delete_permissions_on_project_deletion() { - ComponentDto project = createAndIndexPrivateProject(); - UserDto user = db.users().insertUser(); - db.users().insertProjectPermissionOnUser(user, USER, project); - indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); - verifyAuthorized(project, user); - - db.getDbClient().componentDao().delete(db.getSession(), project.getId()); - indexPermissions(project, ProjectIndexer.Cause.PROJECT_DELETION); - - verifyNotAuthorized(project, user); - assertThatAuthIndexHasSize(0); - } - - @Test - public void errors_during_indexing_are_recovered() { - ComponentDto project = createAndIndexPublicProject(); - es.lockWrites(INDEX_TYPE_FOO_AUTH); - - IndexingResult result = indexPermissions(project, PERMISSION_CHANGE); - assertThat(result.getTotal()).isEqualTo(1L); - assertThat(result.getFailures()).isEqualTo(1L); - - // index is still read-only, fail to recover - result = recover(); - assertThat(result.getTotal()).isEqualTo(1L); - assertThat(result.getFailures()).isEqualTo(1L); - assertThatAuthIndexHasSize(0); - assertThatEsQueueTableHasSize(1); - - es.unlockWrites(INDEX_TYPE_FOO_AUTH); - - result = recover(); - assertThat(result.getTotal()).isEqualTo(1L); - assertThat(result.getFailures()).isEqualTo(0L); - verifyAnyoneAuthorized(project); - assertThatEsQueueTableHasSize(0); - } - - private void assertThatAuthIndexHasSize(int expectedSize) { - IndexType authIndexType = underTest.getIndexTypes().iterator().next(); - assertThat(es.countDocuments(authIndexType)).isEqualTo(expectedSize); - } - - private void indexOnStartup() { - underTest.indexOnStartup(underTest.getIndexTypes()); - } - - private void verifyAuthorized(ComponentDto project, UserDto user) { - logIn(user); - verifyAuthorized(project, true); - } - - private void verifyAuthorized(ComponentDto project, UserDto user, GroupDto group) { - logIn(user).setGroups(group); - verifyAuthorized(project, true); - } - - private void verifyNotAuthorized(ComponentDto project, UserDto user) { - logIn(user); - verifyAuthorized(project, false); - } - - private void verifyNotAuthorized(ComponentDto project, UserDto user, GroupDto group) { - logIn(user).setGroups(group); - verifyAuthorized(project, false); - } - - private void verifyAnyoneAuthorized(ComponentDto project) { - userSession.anonymous(); - verifyAuthorized(project, true); - } - - private void verifyAnyoneNotAuthorized(ComponentDto project) { - userSession.anonymous(); - verifyAuthorized(project, false); - } - - private void verifyAuthorized(ComponentDto project, boolean expectedAccess) { - assertThat(fooIndex.hasAccessToProject(project.uuid())).isEqualTo(expectedAccess); - } - - private LightUserSessionRule logIn(UserDto u) { - userSession.logIn(u.getLogin()).setUserId(u.getId()); - return userSession; - } - - private IndexingResult indexPermissions(ComponentDto project, ProjectIndexer.Cause cause) { - DbSession dbSession = db.getSession(); - Collection items = underTest.prepareForRecovery(dbSession, singletonList(project.uuid()), cause); - dbSession.commit(); - return underTest.index(dbSession, items); - } - - private ComponentDto createUnindexedPublicProject() { - ComponentDto project = db.components().insertPublicProject(); - return project; - } - - private ComponentDto createAndIndexPrivateProject() { - ComponentDto project = db.components().insertPrivateProject(); - fooIndexer.indexOnAnalysis(project.uuid()); - return project; - } - - private ComponentDto createAndIndexPublicProject() { - ComponentDto project = db.components().insertPublicProject(); - fooIndexer.indexOnAnalysis(project.uuid()); - return project; - } - - private ComponentDto createAndIndexView() { - ComponentDto view = db.components().insertView(); - fooIndexer.indexOnAnalysis(view.uuid()); - return view; - } - - private ComponentDto createAndIndexPublicProject(OrganizationDto org) { - ComponentDto project = db.components().insertPublicProject(org); - fooIndexer.indexOnAnalysis(project.uuid()); - return project; - } - - private IndexingResult recover() { - Collection items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10); - return underTest.index(db.getSession(), items); - } - - private void assertThatEsQueueTableHasSize(int expectedSize) { - assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize); - } - -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java b/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java deleted file mode 100644 index 41a9c6b525e..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.permission.index; - -import java.util.Arrays; -import java.util.stream.Stream; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; -import org.sonar.server.es.EsTester; - -import static java.util.Arrays.asList; - -public class PermissionIndexerTester { - - private final PermissionIndexer permissionIndexer; - - public PermissionIndexerTester(EsTester esTester, NeedAuthorizationIndexer indexer, NeedAuthorizationIndexer... others) { - NeedAuthorizationIndexer[] indexers = Stream.concat(Stream.of(indexer), Arrays.stream(others)).toArray(NeedAuthorizationIndexer[]::new); - this.permissionIndexer = new PermissionIndexer(null, esTester.client(), indexers); - } - - public PermissionIndexerTester allowOnlyAnyone(ComponentDto project) { - PermissionIndexerDao.Dto dto = new PermissionIndexerDao.Dto(project.uuid(), project.qualifier()); - dto.allowAnyone(); - permissionIndexer.index(asList(dto)); - return this; - } - - public PermissionIndexerTester allowOnlyUser(ComponentDto project, UserDto user) { - PermissionIndexerDao.Dto dto = new PermissionIndexerDao.Dto(project.uuid(), project.qualifier()) - .addUserId(user.getId()); - permissionIndexer.index(asList(dto)); - return this; - } - - public PermissionIndexerTester allowOnlyGroup(ComponentDto project, GroupDto group) { - PermissionIndexerDao.Dto dto = new PermissionIndexerDao.Dto(project.uuid(), project.qualifier()) - .addGroupId(group.getId()); - permissionIndexer.index(asList(dto)); - return this; - } - - public PermissionIndexerTester allow(PermissionIndexerDao.Dto access) { - permissionIndexer.index(asList(access)); - return this; - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/user/LightUserSessionRule.java b/server/sonar-server-common/src/test/java/org/sonar/server/user/LightUserSessionRule.java deleted file mode 100644 index 640dcb9aa91..00000000000 --- a/server/sonar-server-common/src/test/java/org/sonar/server/user/LightUserSessionRule.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.user; - -import com.google.common.collect.HashMultimap; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import javax.annotation.CheckForNull; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; -import org.sonar.api.web.UserRole; -import org.sonar.db.component.ComponentDto; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.permission.OrganizationPermission; -import org.sonar.db.user.GroupDto; -import org.sonar.db.user.UserDto; - -import static com.google.common.collect.Maps.newHashMap; -import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; - -public class LightUserSessionRule extends BaseUserSession implements TestRule { - private HashMultimap projectUuidByPermission = HashMultimap.create(); - private Set projectPermissionsCheckedByUuid = new HashSet<>(); - private Map projectUuidByComponentUuid = newHashMap(); - private boolean root = false; - private String login; - private Integer userId; - private String uuid; - private String name; - private Set groups = new HashSet<>(); - - public Statement apply(final Statement base, Description description) { - return new Statement() { - @Override - public void evaluate() throws Throwable { - reset(); - try { - base.evaluate(); - } finally { - reset(); - } - } - }; - } - - private void reset() { - this.root = false; - this.login = null; - this.userId = null; - this.userId = null; - this.name = null; - this.groups.clear(); - } - - public LightUserSessionRule setRoot() { - this.root = true; - return this; - } - - public LightUserSessionRule anonymous() { - reset(); - return this; - } - - public LightUserSessionRule logIn() { - return logIn(randomAlphabetic(6)); - } - - public LightUserSessionRule logIn(String login) { - reset(); - this.login = login; - return this; - } - - public LightUserSessionRule logIn(UserDto userDto) { - reset(); - logIn(userDto.getLogin()); - setUserId(userDto.getId()); - return this; - } - - public LightUserSessionRule setUserId(Integer userId) { - this.userId = userId; - return this; - } - - public LightUserSessionRule setUuid(String uuid) { - this.uuid = uuid; - return this; - } - - public LightUserSessionRule setName(String name) { - this.name = name; - return this; - } - - public LightUserSessionRule setGroups(GroupDto... groups) { - this.groups.clear(); - this.groups.addAll(Arrays.asList(groups)); - return this; - } - - /** - * Use this method to register public root component and non root components the UserSession must be aware of. - * (ie. this method can be used to emulate the content of the DB) - */ - public LightUserSessionRule registerComponents(ComponentDto... components) { - Arrays.stream(components) - .forEach(component -> { - if (component.projectUuid().equals(component.uuid()) && !component.isPrivate()) { - this.projectUuidByPermission.put(UserRole.USER, component.uuid()); - this.projectUuidByPermission.put(UserRole.CODEVIEWER, component.uuid()); - this.projectPermissionsCheckedByUuid.add(UserRole.USER); - this.projectPermissionsCheckedByUuid.add(UserRole.CODEVIEWER); - } - this.projectUuidByComponentUuid.put(component.uuid(), component.projectUuid()); - }); - return this; - } - - @Override - protected boolean hasPermissionImpl(OrganizationPermission permission, String organizationUuid) { - throw new UnsupportedOperationException("hasPermissionImpl not implemented"); - } - - @Override - protected boolean hasMembershipImpl(OrganizationDto organization) { - throw new UnsupportedOperationException("hasMembershipImpl not implemented"); - } - - @Override - protected Optional componentUuidToProjectUuid(String componentUuid) { - return Optional.ofNullable(projectUuidByComponentUuid.get(componentUuid)); - } - - @Override - protected boolean hasProjectUuidPermission(String permission, String projectUuid) { - return projectPermissionsCheckedByUuid.contains(permission) && projectUuidByPermission.get(permission).contains(projectUuid); - } - - @CheckForNull - @Override - public String getLogin() { - return login; - } - - @CheckForNull - @Override - public String getUuid() { - return uuid; - } - - @CheckForNull - @Override - public String getName() { - return name; - } - - @CheckForNull - @Override - public Integer getUserId() { - return userId; - } - - @Override - public Collection getGroups() { - return groups; - } - - @Override - public boolean isLoggedIn() { - return login != null; - } - - @Override - public boolean isRoot() { - return root; - } - - @Override - public UserSession checkIsRoot() { - throw new UnsupportedOperationException("checkIsRoot not implemented"); - } - - @Override - public UserSession checkLoggedIn() { - throw new UnsupportedOperationException("checkLoggedIn not implemented"); - } - - @Override - public UserSession checkPermission(OrganizationPermission permission, OrganizationDto organization) { - throw new UnsupportedOperationException("checkPermission not implemented"); - } - - @Override - public UserSession checkPermission(OrganizationPermission permission, String organizationUuid) { - throw new UnsupportedOperationException("checkPermission not implemented"); - } - - @Override - public UserSession checkComponentPermission(String projectPermission, ComponentDto component) { - throw new UnsupportedOperationException("checkComponentPermission not implemented"); - } - - @Override - public UserSession checkComponentUuidPermission(String permission, String componentUuid) { - throw new UnsupportedOperationException("checkComponentUuidPermission not implemented"); - } - - @Override - public boolean isSystemAdministrator() { - throw new UnsupportedOperationException("isSystemAdministrator not implemented"); - } - - @Override - public UserSession checkIsSystemAdministrator() { - throw new UnsupportedOperationException("checkIsSystemAdministrator not implemented"); - } - - @Override - public UserSession checkMembership(OrganizationDto organization) { - throw new UnsupportedOperationException("checkMembership not implemented"); - } -} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java index 00638cd8e3b..6a3e75e6b25 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/view/index/ViewIndexerTest.java @@ -22,32 +22,16 @@ package org.sonar.server.view.index; import com.google.common.collect.Maps; import java.util.List; import java.util.Map; -import org.elasticsearch.action.search.SearchResponse; import org.junit.Rule; import org.junit.Test; import org.junit.rules.DisableOnDebug; import org.junit.rules.TestRule; import org.junit.rules.Timeout; -import org.sonar.api.utils.System2; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; -import org.sonar.db.component.ComponentTesting; -import org.sonar.db.issue.IssueDto; -import org.sonar.db.issue.IssueTesting; -import org.sonar.db.organization.OrganizationDto; -import org.sonar.db.rule.RuleDto; -import org.sonar.db.rule.RuleTesting; import org.sonar.server.es.EsTester; -import org.sonar.server.es.SearchOptions; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.index.IssueIndex; -import org.sonar.server.issue.index.IssueIndexer; -import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexer; -import org.sonar.server.user.LightUserSessionRule; import static com.google.common.collect.Lists.newArrayList; import static java.util.Arrays.asList; @@ -66,13 +50,9 @@ public class ViewIndexerTest { public DbTester db = DbTester.create(); @Rule public EsTester es = EsTester.create(); - @Rule - public LightUserSessionRule userSessionRule = new LightUserSessionRule(); private DbClient dbClient = db.getDbClient(); private DbSession dbSession = db.getSession(); - private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); - private PermissionIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer); private ViewIndexer underTest = new ViewIndexer(dbClient, es.client()); @Test @@ -180,46 +160,6 @@ public class ViewIndexerTest { tuple(applicationBranch1.uuid(), asList(project1Branch.uuid(), project2Branch.uuid()))); } - @Test - public void clear_views_lookup_cache_on_index_view_uuid() { - IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); - IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); - - String viewUuid = "ABCD"; - - RuleDto rule = RuleTesting.newXooX1(); - dbClient.ruleDao().insert(dbSession, rule.getDefinition()); - ComponentDto project1 = addProjectWithIssue(rule, db.organizations().insert()); - issueIndexer.indexOnStartup(issueIndexer.getIndexTypes()); - permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes()); - - OrganizationDto organizationDto = db.organizations().insert(); - ComponentDto view = ComponentTesting.newView(organizationDto, "ABCD"); - ComponentDto techProject1 = newProjectCopy("CDEF", project1, view); - dbClient.componentDao().insert(dbSession, view, techProject1); - dbSession.commit(); - - // First view indexation - underTest.index(viewUuid); - - // Execute issue query on view -> 1 issue on view - SearchResponse issueResponse = issueIndex.search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(), new SearchOptions()); - assertThat(issueResponse.getHits().getHits()).hasSize(1); - - // Add a project to the view and index it again - ComponentDto project2 = addProjectWithIssue(rule, organizationDto); - issueIndexer.indexOnStartup(issueIndexer.getIndexTypes()); - permissionIndexer.indexOnStartup(permissionIndexer.getIndexTypes()); - - ComponentDto techProject2 = newProjectCopy("EFGH", project2, view); - dbClient.componentDao().insert(dbSession, techProject2); - dbSession.commit(); - underTest.index(viewUuid); - - // Execute issue query on view -> issue of project2 are well taken into account : the cache has been cleared - assertThat(issueIndex.search(IssueQuery.builder().viewUuids(newArrayList(viewUuid)).build(), new SearchOptions()).getHits()).hasSize(2); - } - @Test public void delete_should_delete_the_view() { ViewDoc view1 = new ViewDoc().setUuid("UUID1").setProjects(asList("P1")); @@ -238,16 +178,4 @@ public class ViewIndexerTest { .containsOnly(view3.uuid()); } - private ComponentDto addProjectWithIssue(RuleDto rule, OrganizationDto org) { - ComponentDto project = ComponentTesting.newPublicProjectDto(org); - ComponentDto file = ComponentTesting.newFileDto(project, null); - db.components().insertComponents(project, file); - - IssueDto issue = IssueTesting.newDto(rule, file, project); - dbClient.issueDao().insert(dbSession, issue); - dbSession.commit(); - - return project; - } - } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java new file mode 100644 index 00000000000..b7a20f64126 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndex.java @@ -0,0 +1,209 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregator.KeyedFilter; +import org.elasticsearch.search.aggregations.bucket.filters.InternalFilters; +import org.elasticsearch.search.aggregations.bucket.filters.InternalFilters.InternalBucket; +import org.elasticsearch.search.aggregations.metrics.tophits.InternalTopHits; +import org.elasticsearch.search.aggregations.metrics.tophits.TopHitsAggregationBuilder; +import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder; +import org.elasticsearch.search.sort.FieldSortBuilder; +import org.elasticsearch.search.sort.ScoreSortBuilder; +import org.elasticsearch.search.sort.SortOrder; +import org.sonar.api.utils.System2; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.SearchIdResult; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.es.textsearch.ComponentTextSearchFeature; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; +import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory; +import org.sonar.server.es.textsearch.ComponentTextSearchQueryFactory.ComponentTextSearchQuery; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; + +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.index.query.QueryBuilders.termsQuery; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_LANGUAGE; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_NAME; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_ORGANIZATION_UUID; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_QUALIFIER; +import static org.sonar.server.component.index.ComponentIndexDefinition.INDEX_TYPE_COMPONENT; +import static org.sonar.server.component.index.ComponentIndexDefinition.NAME_ANALYZERS; +import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; + +public class ComponentIndex { + + private static final String FILTERS_AGGREGATION_NAME = "filters"; + private static final String DOCS_AGGREGATION_NAME = "docs"; + + private final EsClient client; + private final WebAuthorizationTypeSupport authorizationTypeSupport; + private final System2 system2; + + public ComponentIndex(EsClient client, WebAuthorizationTypeSupport authorizationTypeSupport, System2 system2) { + this.client = client; + this.authorizationTypeSupport = authorizationTypeSupport; + this.system2 = system2; + } + + public SearchIdResult search(ComponentQuery query, SearchOptions searchOptions) { + SearchRequestBuilder requestBuilder = client + .prepareSearch(INDEX_TYPE_COMPONENT) + .setFetchSource(false) + .setFrom(searchOptions.getOffset()) + .setSize(searchOptions.getLimit()); + + BoolQueryBuilder esQuery = boolQuery(); + esQuery.filter(authorizationTypeSupport.createQueryFilter()); + setNullable(query.getQuery(), q -> { + ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQuery.builder() + .setQueryText(q) + .setFieldKey(FIELD_KEY) + .setFieldName(FIELD_NAME) + .build(); + esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, ComponentTextSearchFeatureRepertoire.values())); + }); + setEmptiable(query.getQualifiers(), q -> esQuery.must(termsQuery(FIELD_QUALIFIER, q))); + setNullable(query.getLanguage(), l -> esQuery.must(termQuery(FIELD_LANGUAGE, l))); + setNullable(query.getOrganizationUuid(), o -> esQuery.must(termQuery(FIELD_ORGANIZATION_UUID, o))); + requestBuilder.setQuery(esQuery); + requestBuilder.addSort(SORTABLE_ANALYZER.subField(FIELD_NAME), SortOrder.ASC); + + return new SearchIdResult<>(requestBuilder.get(), id -> id, system2.getDefaultTimeZone()); + } + + public ComponentIndexResults searchSuggestions(SuggestionQuery query) { + return searchSuggestions(query, ComponentTextSearchFeatureRepertoire.values()); + } + + @VisibleForTesting + ComponentIndexResults searchSuggestions(SuggestionQuery query, ComponentTextSearchFeature... features) { + Collection qualifiers = query.getQualifiers(); + if (qualifiers.isEmpty()) { + return ComponentIndexResults.newBuilder().build(); + } + + SearchRequestBuilder request = client + .prepareSearch(INDEX_TYPE_COMPONENT) + .setQuery(createQuery(query, features)) + .addAggregation(createAggregation(query)) + + // the search hits are part of the aggregations + .setSize(0); + + SearchResponse response = request.get(); + + return aggregationsToQualifiers(response); + } + + private static HighlightBuilder.Field createHighlighterField() { + HighlightBuilder.Field field = new HighlightBuilder.Field(FIELD_NAME); + field.highlighterType("fvh"); + field.matchedFields( + Stream.concat( + Stream.of(FIELD_NAME), + Arrays + .stream(NAME_ANALYZERS) + .map(a -> a.subField(FIELD_NAME))) + .toArray(String[]::new)); + return field; + } + + private static FiltersAggregationBuilder createAggregation(SuggestionQuery query) { + return AggregationBuilders.filters( + FILTERS_AGGREGATION_NAME, + query.getQualifiers().stream().map(q -> new KeyedFilter(q, termQuery(FIELD_QUALIFIER, q))).toArray(KeyedFilter[]::new)) + .subAggregation(createSubAggregation(query)); + } + + private static TopHitsAggregationBuilder createSubAggregation(SuggestionQuery query) { + return AggregationBuilders.topHits(DOCS_AGGREGATION_NAME) + .highlighter(new HighlightBuilder() + .encoder("html") + .preTags("") + .postTags("") + .field(createHighlighterField())) + .from(query.getSkip()) + .size(query.getLimit()) + .sort(new ScoreSortBuilder()) + .sort(new FieldSortBuilder(ComponentIndexDefinition.FIELD_NAME)) + .fetchSource(false); + } + + private QueryBuilder createQuery(SuggestionQuery query, ComponentTextSearchFeature... features) { + BoolQueryBuilder esQuery = boolQuery(); + esQuery.filter(authorizationTypeSupport.createQueryFilter()); + ComponentTextSearchQuery componentTextSearchQuery = ComponentTextSearchQuery.builder() + .setQueryText(query.getQuery()) + .setFieldKey(FIELD_KEY) + .setFieldName(FIELD_NAME) + .setRecentlyBrowsedKeys(query.getRecentlyBrowsedKeys()) + .setFavoriteKeys(query.getFavoriteKeys()) + .build(); + return esQuery.must(ComponentTextSearchQueryFactory.createQuery(componentTextSearchQuery, features)); + } + + private static ComponentIndexResults aggregationsToQualifiers(SearchResponse response) { + InternalFilters filtersAgg = response.getAggregations().get(FILTERS_AGGREGATION_NAME); + List buckets = filtersAgg.getBuckets(); + return ComponentIndexResults.newBuilder() + .setQualifiers( + buckets.stream().map(ComponentIndex::bucketToQualifier)) + .build(); + } + + private static ComponentHitsPerQualifier bucketToQualifier(InternalBucket bucket) { + InternalTopHits docs = bucket.getAggregations().get(DOCS_AGGREGATION_NAME); + + SearchHits hitList = docs.getHits(); + SearchHit[] hits = hitList.getHits(); + + return new ComponentHitsPerQualifier(bucket.getKey(), ComponentHit.fromSearchHits(hits), hitList.getTotalHits()); + } + + private static void setNullable(@Nullable T parameter, Consumer consumer) { + if (parameter != null) { + consumer.accept(parameter); + } + } + + private static void setEmptiable(Collection parameter, Consumer> consumer) { + if (!parameter.isEmpty()) { + consumer.accept(parameter); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java new file mode 100644 index 00000000000..9154f3b1247 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentIndexResults.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.util.Collections.emptyList; +import static java.util.Objects.requireNonNull; + +public class ComponentIndexResults { + + private final List qualifiers; + + private ComponentIndexResults(Builder builder) { + this.qualifiers = requireNonNull(builder.qualifiers); + } + + public Stream getQualifiers() { + return qualifiers.stream(); + } + + public boolean isEmpty() { + return qualifiers.isEmpty(); + } + + public static Builder newBuilder() { + return new Builder(); + } + + public static class Builder { + + private List qualifiers = emptyList(); + + private Builder() { + } + + public Builder setQualifiers(Stream qualifiers) { + this.qualifiers = qualifiers.collect(Collectors.toList()); + return this; + } + + public ComponentIndexResults build() { + return new ComponentIndexResults(this); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentQuery.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentQuery.java new file mode 100644 index 00000000000..809362008ab --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentQuery.java @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Collection; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import static java.util.Collections.emptySet; +import static java.util.Collections.unmodifiableCollection; + +public class ComponentQuery { + private final String organizationUuid; + private final String query; + private final Collection qualifiers; + private final String language; + + private ComponentQuery(Builder builder) { + this.organizationUuid = builder.organizationUuid; + this.query = builder.query; + this.qualifiers = builder.qualifiers; + this.language = builder.language; + } + + @CheckForNull + public String getOrganizationUuid() { + return organizationUuid; + } + + @CheckForNull + public String getQuery() { + return query; + } + + public Collection getQualifiers() { + return qualifiers; + } + + @CheckForNull + public String getLanguage() { + return language; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String organizationUuid; + private String query; + private Collection qualifiers = emptySet(); + private String language; + + private Builder() { + // enforce static factory method + } + + public Builder setOrganization(@Nullable String organizationUuid) { + this.organizationUuid = organizationUuid; + return this; + } + + public Builder setQuery(@Nullable String query) { + this.query = query; + return this; + } + + public Builder setQualifiers(Collection qualifiers) { + this.qualifiers = unmodifiableCollection(qualifiers); + return this; + } + + public Builder setLanguage(@Nullable String language) { + this.language = language; + return this; + } + + public ComponentQuery build() { + return new ComponentQuery(this); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/package-info.java new file mode 100644 index 00000000000..54f6c6e1bae --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/IssuesFinderSort.java b/server/sonar-server/src/main/java/org/sonar/server/issue/IssuesFinderSort.java index 21004ab49da..2dbd6604799 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/IssuesFinderSort.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/IssuesFinderSort.java @@ -27,6 +27,7 @@ import java.util.List; import javax.annotation.Nonnull; import org.sonar.api.rule.Severity; import org.sonar.db.issue.IssueDto; +import org.sonar.server.issue.index.IssueQuery; /** * @since 3.6 diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java new file mode 100644 index 00000000000..f623f129bd3 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueIndex.java @@ -0,0 +1,918 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.BooleanUtils; +import org.apache.commons.lang.StringUtils; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.indices.TermsLookup; +import org.elasticsearch.search.aggregations.AggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.HasAggregations; +import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.filter.InternalFilter; +import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval; +import org.elasticsearch.search.aggregations.bucket.histogram.ExtendedBounds; +import org.elasticsearch.search.aggregations.bucket.terms.InternalTerms; +import org.elasticsearch.search.aggregations.bucket.terms.StringTerms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.Terms.Order; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.metrics.max.InternalMax; +import org.elasticsearch.search.aggregations.metrics.min.Min; +import org.elasticsearch.search.aggregations.metrics.sum.SumAggregationBuilder; +import org.elasticsearch.search.aggregations.metrics.valuecount.InternalValueCount; +import org.elasticsearch.search.sort.FieldSortBuilder; +import org.joda.time.DateTimeZone; +import org.joda.time.Duration; +import org.sonar.api.issue.Issue; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.DateUtils; +import org.sonar.api.utils.System2; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.server.es.BaseDoc; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.EsUtils; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.es.Sorting; +import org.sonar.server.es.StickyFacetBuilder; +import org.sonar.server.issue.index.IssueQuery.PeriodStart; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.user.UserSession; +import org.sonar.server.view.index.ViewIndexDefinition; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.existsQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.index.query.QueryBuilders.termsQuery; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonar.server.es.BaseDoc.epochMillisToEpochSeconds; +import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_INSECURE_INTERACTION; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_POROUS_DEFENSES; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_RISKY_RESOURCE; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; +import static org.sonar.server.issue.index.IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID; +import static org.sonar.server.issue.index.IssueIndexDefinition.INDEX_TYPE_ISSUE; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_PARAM_ACTION_PLANS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_ASSIGNED_TO_ME; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ASSIGNEES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_AUTHORS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AT; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CWE; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_DIRECTORIES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_FILE_UUIDS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_LANGUAGES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_MODULE_UUIDS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_UUIDS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_REPORTERS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RESOLUTIONS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SEVERITIES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_STATUSES; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TAGS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TYPES; + +/** + * The unique entry-point to interact with Elasticsearch index "issues". + * All the requests are listed here. + */ +public class IssueIndex { + + public static final List SUPPORTED_FACETS = ImmutableList.of( + PARAM_SEVERITIES, + PARAM_STATUSES, + PARAM_RESOLUTIONS, + DEPRECATED_PARAM_ACTION_PLANS, + PARAM_PROJECT_UUIDS, + PARAM_RULES, + PARAM_ASSIGNEES, + FACET_ASSIGNED_TO_ME, + PARAM_REPORTERS, + PARAM_AUTHORS, + PARAM_MODULE_UUIDS, + PARAM_FILE_UUIDS, + PARAM_DIRECTORIES, + PARAM_LANGUAGES, + PARAM_TAGS, + PARAM_TYPES, + PARAM_OWASP_TOP_10, + PARAM_SANS_TOP_25, + PARAM_CWE, + PARAM_CREATED_AT); + public static final String AGGREGATION_NAME_FOR_TAGS = "tags__issues"; + private static final String SUBSTRING_MATCH_REGEXP = ".*%s.*"; + // TODO to be documented + // TODO move to Facets ? + private static final String FACET_SUFFIX_MISSING = "_missing"; + private static final String IS_ASSIGNED_FILTER = "__isAssigned"; + private static final SumAggregationBuilder EFFORT_AGGREGATION = AggregationBuilders.sum(FACET_MODE_EFFORT).field(IssueIndexDefinition.FIELD_ISSUE_EFFORT); + private static final Order EFFORT_AGGREGATION_ORDER = Order.aggregation(FACET_MODE_EFFORT, false); + private static final int DEFAULT_FACET_SIZE = 15; + private static final Duration TWENTY_DAYS = Duration.standardDays(20L); + private static final Duration TWENTY_WEEKS = Duration.standardDays(20L * 7L); + private static final Duration TWENTY_MONTHS = Duration.standardDays(20L * 30L); + private static final String COUNT = "count"; + private final Sorting sorting; + private final EsClient client; + private final System2 system; + private final UserSession userSession; + private final WebAuthorizationTypeSupport authorizationTypeSupport; + + public IssueIndex(EsClient client, System2 system, UserSession userSession, WebAuthorizationTypeSupport authorizationTypeSupport) { + this.client = client; + this.system = system; + this.userSession = userSession; + this.authorizationTypeSupport = authorizationTypeSupport; + + this.sorting = new Sorting(); + this.sorting.add(IssueQuery.SORT_BY_STATUS, IssueIndexDefinition.FIELD_ISSUE_STATUS); + this.sorting.add(IssueQuery.SORT_BY_SEVERITY, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE); + this.sorting.add(IssueQuery.SORT_BY_CREATION_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT); + this.sorting.add(IssueQuery.SORT_BY_UPDATE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_UPDATED_AT); + this.sorting.add(IssueQuery.SORT_BY_CLOSE_DATE, IssueIndexDefinition.FIELD_ISSUE_FUNC_CLOSED_AT); + this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID); + this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_FILE_PATH); + this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_LINE); + this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_SEVERITY_VALUE).reverse(); + this.sorting.add(IssueQuery.SORT_BY_FILE_LINE, IssueIndexDefinition.FIELD_ISSUE_KEY); + + // by default order by created date, project, file, line and issue key (in order to be deterministic when same ms) + this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).reverse(); + this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID); + this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_FILE_PATH); + this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_LINE); + this.sorting.addDefault(IssueIndexDefinition.FIELD_ISSUE_KEY); + } + + public SearchResponse search(IssueQuery query, SearchOptions options) { + SearchRequestBuilder requestBuilder = client.prepareSearch(INDEX_TYPE_ISSUE); + + configureSorting(query, requestBuilder); + configurePagination(options, requestBuilder); + configureRouting(query, options, requestBuilder); + + QueryBuilder esQuery = matchAllQuery(); + BoolQueryBuilder esFilter = boolQuery(); + Map filters = createFilters(query); + for (QueryBuilder filter : filters.values()) { + if (filter != null) { + esFilter.must(filter); + } + } + if (esFilter.hasClauses()) { + requestBuilder.setQuery(boolQuery().must(esQuery).filter(esFilter)); + } else { + requestBuilder.setQuery(esQuery); + } + + configureStickyFacets(query, options, filters, esQuery, requestBuilder); + requestBuilder.setFetchSource(false); + return requestBuilder.get(); + } + + /** + * Optimization - do not send ES request to all shards when scope is restricted + * to a set of projects. Because project UUID is used for routing, the request + * can be sent to only the shards containing the specified projects. + * Note that sticky facets may involve all projects, so this optimization must be + * disabled when facets are enabled. + */ + private static void configureRouting(IssueQuery query, SearchOptions options, SearchRequestBuilder requestBuilder) { + Collection uuids = query.projectUuids(); + if (!uuids.isEmpty() && options.getFacets().isEmpty()) { + requestBuilder.setRouting(uuids.toArray(new String[uuids.size()])); + } + } + + private static void configurePagination(SearchOptions options, SearchRequestBuilder esSearch) { + esSearch.setFrom(options.getOffset()).setSize(options.getLimit()); + } + + private Map createFilters(IssueQuery query) { + Map filters = new HashMap<>(); + filters.put("__authorization", createAuthorizationFilter(query.checkAuthorization())); + + // Issue is assigned Filter + if (BooleanUtils.isTrue(query.assigned())) { + filters.put(IS_ASSIGNED_FILTER, existsQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID)); + } else if (BooleanUtils.isFalse(query.assigned())) { + filters.put(IS_ASSIGNED_FILTER, boolQuery().mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID))); + } + + // Issue is Resolved Filter + String isResolved = "__isResolved"; + if (BooleanUtils.isTrue(query.resolved())) { + filters.put(isResolved, existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)); + } else if (BooleanUtils.isFalse(query.resolved())) { + filters.put(isResolved, boolQuery().mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))); + } + + // Field Filters + filters.put(IssueIndexDefinition.FIELD_ISSUE_KEY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_KEY, query.issueKeys())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, query.assignees())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_TAGS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_TYPE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_TYPE, query.types())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, query.resolutions())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_RULE_ID, createTermsFilter( + IssueIndexDefinition.FIELD_ISSUE_RULE_ID, + query.rules().stream().map(RuleDefinitionDto::getId).collect(Collectors.toList()))); + filters.put(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_SEVERITY, query.severities())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_STATUS, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_STATUS, query.statuses())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, createTermFilter(IssueIndexDefinition.FIELD_ISSUE_ORGANIZATION_UUID, query.organizationUuid())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, query.owaspTop10())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, query.sansTop25())); + filters.put(IssueIndexDefinition.FIELD_ISSUE_CWE, createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_CWE, query.cwe())); + + addComponentRelatedFilters(query, filters); + + addDatesFilter(filters, query); + addCreatedAfterByProjectsFilter(filters, query); + return filters; + } + + private static void addComponentRelatedFilters(IssueQuery query, Map filters) { + addCommonComponentRelatedFilters(query, filters); + if (query.viewUuids().isEmpty()) { + addBranchComponentRelatedFilters(query, filters); + } else { + addViewRelatedFilters(query, filters); + } + } + + private static void addCommonComponentRelatedFilters(IssueQuery query, Map filters) { + QueryBuilder componentFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.componentUuids()); + QueryBuilder projectFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids()); + QueryBuilder moduleRootFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_PATH, query.moduleRootUuids()); + QueryBuilder moduleFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids()); + QueryBuilder directoryFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories()); + QueryBuilder fileFilter = createTermsFilter(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids()); + + if (BooleanUtils.isTrue(query.onComponentOnly())) { + filters.put(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, componentFilter); + } else { + filters.put(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectFilter); + filters.put("__module", moduleRootFilter); + filters.put(IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, moduleFilter); + filters.put(IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, directoryFilter); + filters.put(IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, fileFilter != null ? fileFilter : componentFilter); + } + } + + private static void addBranchComponentRelatedFilters(IssueQuery query, Map filters) { + if (BooleanUtils.isTrue(query.onComponentOnly())) { + return; + } + QueryBuilder branchFilter = createTermFilter(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, query.branchUuid()); + filters.put("__is_main_branch", createTermFilter(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(query.isMainBranch()))); + filters.put(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, branchFilter); + } + + private static void addViewRelatedFilters(IssueQuery query, Map filters) { + if (BooleanUtils.isTrue(query.onComponentOnly())) { + return; + } + Collection viewUuids = query.viewUuids(); + String branchUuid = query.branchUuid(); + boolean onApplicationBranch = branchUuid != null && !viewUuids.isEmpty(); + if (onApplicationBranch) { + filters.put("__view", createViewFilter(singletonList(query.branchUuid()))); + } else { + filters.put("__is_main_branch", createTermFilter(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(true))); + filters.put("__view", createViewFilter(viewUuids)); + } + } + + @CheckForNull + private static QueryBuilder createViewFilter(Collection viewUuids) { + if (viewUuids.isEmpty()) { + return null; + } + + BoolQueryBuilder viewsFilter = boolQuery(); + for (String viewUuid : viewUuids) { + viewsFilter.should(QueryBuilders.termsLookupQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, + new TermsLookup( + ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), + ViewIndexDefinition.INDEX_TYPE_VIEW.getType(), + viewUuid, + ViewIndexDefinition.FIELD_PROJECTS))); + } + return viewsFilter; + } + + private static StickyFacetBuilder newStickyFacetBuilder(IssueQuery query, Map filters, QueryBuilder esQuery) { + if (hasQueryEffortFacet(query)) { + return new StickyFacetBuilder(esQuery, filters, EFFORT_AGGREGATION, EFFORT_AGGREGATION_ORDER); + } + return new StickyFacetBuilder(esQuery, filters); + } + + private static void addSimpleStickyFacetIfNeeded(SearchOptions options, StickyFacetBuilder stickyFacetBuilder, SearchRequestBuilder esSearch, + String facetName, String fieldName, Object... selectedValues) { + if (options.getFacets().contains(facetName)) { + esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldName, facetName, DEFAULT_FACET_SIZE, selectedValues)); + } + } + + private static AggregationBuilder addEffortAggregationIfNeeded(IssueQuery query, AggregationBuilder aggregation) { + if (hasQueryEffortFacet(query)) { + aggregation.subAggregation(EFFORT_AGGREGATION); + } + return aggregation; + } + + private static boolean hasQueryEffortFacet(IssueQuery query) { + return FACET_MODE_EFFORT.equals(query.facetMode()) || DEPRECATED_FACET_MODE_DEBT.equals(query.facetMode()); + } + + private static AggregationBuilder createAssigneesFacet(IssueQuery query, Map filters, QueryBuilder queryBuilder) { + String fieldName = IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID; + String facetName = PARAM_ASSIGNEES; + + // Same as in super.stickyFacetBuilder + Map assigneeFilters = Maps.newHashMap(filters); + assigneeFilters.remove(IS_ASSIGNED_FILTER); + assigneeFilters.remove(fieldName); + StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, assigneeFilters, queryBuilder); + BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); + FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE); + + Collection assigneesEscaped = escapeValuesForFacetInclusion(query.assignees()); + if (!assigneesEscaped.isEmpty()) { + facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t, assigneesEscaped.toArray()); + } + + // Add missing facet for unassigned issues + facetTopAggregation.subAggregation( + addEffortAggregationIfNeeded(query, AggregationBuilders + .missing(facetName + FACET_SUFFIX_MISSING) + .field(fieldName))); + + return AggregationBuilders + .global(facetName) + .subAggregation(facetTopAggregation); + } + + private static Collection escapeValuesForFacetInclusion(@Nullable Collection values) { + if (values == null) { + return Collections.emptyList(); + } + return values.stream().map(Pattern::quote).collect(MoreCollectors.toArrayList(values.size())); + } + + private static AggregationBuilder createResolutionFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { + String fieldName = IssueIndexDefinition.FIELD_ISSUE_RESOLUTION; + String facetName = PARAM_RESOLUTIONS; + + // Same as in super.stickyFacetBuilder + Map resolutionFilters = Maps.newHashMap(filters); + resolutionFilters.remove("__isResolved"); + resolutionFilters.remove(fieldName); + StickyFacetBuilder assigneeFacetBuilder = newStickyFacetBuilder(query, resolutionFilters, esQuery); + BoolQueryBuilder facetFilter = assigneeFacetBuilder.getStickyFacetFilter(fieldName); + FilterAggregationBuilder facetTopAggregation = assigneeFacetBuilder.buildTopFacetAggregation(fieldName, facetName, facetFilter, DEFAULT_FACET_SIZE); + facetTopAggregation = assigneeFacetBuilder.addSelectedItemsToFacet(fieldName, facetName, facetTopAggregation, t -> t); + + // Add missing facet for unresolved issues + facetTopAggregation.subAggregation( + addEffortAggregationIfNeeded(query, AggregationBuilders + .missing(facetName + FACET_SUFFIX_MISSING) + .field(fieldName))); + + return AggregationBuilders + .global(facetName) + .subAggregation(facetTopAggregation); + } + + @CheckForNull + private static QueryBuilder createTermsFilter(String field, Collection values) { + return values.isEmpty() ? null : termsQuery(field, values); + } + + @CheckForNull + private static QueryBuilder createTermFilter(String field, @Nullable String value) { + return value == null ? null : termQuery(field, value); + } + + private void configureSorting(IssueQuery query, SearchRequestBuilder esRequest) { + createSortBuilders(query).forEach(esRequest::addSort); + } + + private List createSortBuilders(IssueQuery query) { + String sortField = query.sort(); + if (sortField != null) { + boolean asc = BooleanUtils.isTrue(query.asc()); + return sorting.fill(sortField, asc); + } + return sorting.fillDefault(); + } + + private QueryBuilder createAuthorizationFilter(boolean checkAuthorization) { + if (checkAuthorization) { + return authorizationTypeSupport.createQueryFilter(); + } + return matchAllQuery(); + } + + private void addDatesFilter(Map filters, IssueQuery query) { + PeriodStart createdAfter = query.createdAfter(); + Date createdBefore = query.createdBefore(); + + validateCreationDateBounds(createdBefore, createdAfter != null ? createdAfter.date() : null); + + if (createdAfter != null) { + filters.put("__createdAfter", QueryBuilders + .rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) + .from(BaseDoc.dateToEpochSeconds(createdAfter.date()), createdAfter.inclusive())); + } + if (createdBefore != null) { + filters.put("__createdBefore", QueryBuilders + .rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) + .lt(BaseDoc.dateToEpochSeconds(createdBefore))); + } + Date createdAt = query.createdAt(); + if (createdAt != null) { + filters.put("__createdAt", termQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT, BaseDoc.dateToEpochSeconds(createdAt))); + } + } + + private static void addCreatedAfterByProjectsFilter(Map filters, IssueQuery query) { + Map createdAfterByProjectUuids = query.createdAfterByProjectUuids(); + BoolQueryBuilder boolQueryBuilder = boolQuery(); + createdAfterByProjectUuids.forEach((projectUuid, createdAfterDate) -> boolQueryBuilder.should(boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid)) + .filter(rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).from(BaseDoc.dateToEpochSeconds(createdAfterDate.date()), createdAfterDate.inclusive())))); + filters.put("createdAfterByProjectUuids", boolQueryBuilder); + } + + private void validateCreationDateBounds(@Nullable Date createdBefore, @Nullable Date createdAfter) { + Preconditions.checkArgument(createdAfter == null || createdAfter.before(new Date(system.now())), + "Start bound cannot be in the future"); + Preconditions.checkArgument(createdAfter == null || createdBefore == null || createdAfter.before(createdBefore), + "Start bound cannot be larger or equal to end bound"); + } + + private void configureStickyFacets(IssueQuery query, SearchOptions options, Map filters, QueryBuilder esQuery, SearchRequestBuilder esSearch) { + if (!options.getFacets().isEmpty()) { + StickyFacetBuilder stickyFacetBuilder = newStickyFacetBuilder(query, filters, esQuery); + // Execute Term aggregations + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_SEVERITIES, IssueIndexDefinition.FIELD_ISSUE_SEVERITY); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_STATUSES, IssueIndexDefinition.FIELD_ISSUE_STATUS); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_PROJECT_UUIDS, IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, query.projectUuids().toArray()); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_MODULE_UUIDS, IssueIndexDefinition.FIELD_ISSUE_MODULE_UUID, query.moduleUuids().toArray()); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_DIRECTORIES, IssueIndexDefinition.FIELD_ISSUE_DIRECTORY_PATH, query.directories().toArray()); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_FILE_UUIDS, IssueIndexDefinition.FIELD_ISSUE_COMPONENT_UUID, query.fileUuids().toArray()); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_LANGUAGES, IssueIndexDefinition.FIELD_ISSUE_LANGUAGE, query.languages().toArray()); + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_RULES, IssueIndexDefinition.FIELD_ISSUE_RULE_ID, query.rules().stream().map(RuleDefinitionDto::getId).toArray()); + + addSimpleStickyFacetIfNeeded(options, stickyFacetBuilder, esSearch, + PARAM_AUTHORS, IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query.authors().toArray()); + + addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_TAGS, IssueIndexDefinition.FIELD_ISSUE_TAGS, query.tags()); + addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_TYPES, IssueIndexDefinition.FIELD_ISSUE_TYPE, query.types()); + addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_OWASP_TOP_10, IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, query.owaspTop10()); + addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_SANS_TOP_25, IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, query.sansTop25()); + addStickyFacetIfNeeded(options, esSearch, stickyFacetBuilder, PARAM_CWE, IssueIndexDefinition.FIELD_ISSUE_CWE, query.cwe()); + if (options.getFacets().contains(PARAM_RESOLUTIONS)) { + esSearch.addAggregation(createResolutionFacet(query, filters, esQuery)); + } + if (options.getFacets().contains(PARAM_ASSIGNEES)) { + esSearch.addAggregation(createAssigneesFacet(query, filters, esQuery)); + } + addAssignedToMeFacetIfNeeded(esSearch, options, query, filters, esQuery); + if (options.getFacets().contains(PARAM_CREATED_AT)) { + getCreatedAtFacet(query, filters, esQuery).ifPresent(esSearch::addAggregation); + } + } + + if (hasQueryEffortFacet(query)) { + esSearch.addAggregation(EFFORT_AGGREGATION); + } + } + + private static void addStickyFacetIfNeeded(SearchOptions options, SearchRequestBuilder esSearch, StickyFacetBuilder stickyFacetBuilder, String paramTags, String fieldIssueTags, + Collection tags) { + if (options.getFacets().contains(paramTags)) { + esSearch.addAggregation(stickyFacetBuilder.buildStickyFacet(fieldIssueTags, paramTags, tags.toArray())); + } + } + + private Optional getCreatedAtFacet(IssueQuery query, Map filters, QueryBuilder esQuery) { + long startTime; + boolean startInclusive; + PeriodStart createdAfter = query.createdAfter(); + if (createdAfter == null) { + Optional minDate = getMinCreatedAt(filters, esQuery); + if (!minDate.isPresent()) { + return Optional.empty(); + } + startTime = minDate.get(); + startInclusive = true; + } else { + startTime = createdAfter.date().getTime(); + startInclusive = createdAfter.inclusive(); + } + Date createdBefore = query.createdBefore(); + long endTime = createdBefore == null ? system.now() : createdBefore.getTime(); + + Duration timeSpan = new Duration(startTime, endTime); + DateHistogramInterval bucketSize = DateHistogramInterval.YEAR; + if (timeSpan.isShorterThan(TWENTY_DAYS)) { + bucketSize = DateHistogramInterval.DAY; + } else if (timeSpan.isShorterThan(TWENTY_WEEKS)) { + bucketSize = DateHistogramInterval.WEEK; + } else if (timeSpan.isShorterThan(TWENTY_MONTHS)) { + bucketSize = DateHistogramInterval.MONTH; + } + + AggregationBuilder dateHistogram = AggregationBuilders.dateHistogram(PARAM_CREATED_AT) + .field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT) + .dateHistogramInterval(bucketSize) + .minDocCount(0L) + .format(DateUtils.DATETIME_FORMAT) + .timeZone(DateTimeZone.forOffsetMillis(system.getDefaultTimeZone().getRawOffset())) + // ES dateHistogram bounds are inclusive while createdBefore parameter is exclusive + .extendedBounds(new ExtendedBounds(startInclusive ? startTime : (startTime + 1), endTime - 1L)); + addEffortAggregationIfNeeded(query, dateHistogram); + return Optional.of(dateHistogram); + } + + private Optional getMinCreatedAt(Map filters, QueryBuilder esQuery) { + String facetNameAndField = IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT; + SearchRequestBuilder esRequest = client + .prepareSearch(INDEX_TYPE_ISSUE) + .setSize(0); + BoolQueryBuilder esFilter = boolQuery(); + filters.values().stream().filter(Objects::nonNull).forEach(esFilter::must); + if (esFilter.hasClauses()) { + esRequest.setQuery(QueryBuilders.boolQuery().must(esQuery).filter(esFilter)); + } else { + esRequest.setQuery(esQuery); + } + esRequest.addAggregation(AggregationBuilders.min(facetNameAndField).field(facetNameAndField)); + Min minValue = esRequest.get().getAggregations().get(facetNameAndField); + + Double actualValue = minValue.getValue(); + if (actualValue.isInfinite()) { + return Optional.empty(); + } + return Optional.of(actualValue.longValue()); + } + + private void addAssignedToMeFacetIfNeeded(SearchRequestBuilder builder, SearchOptions options, IssueQuery query, Map filters, QueryBuilder queryBuilder) { + String uuid = userSession.getUuid(); + + if (!options.getFacets().contains(FACET_ASSIGNED_TO_ME) || StringUtils.isEmpty(uuid)) { + return; + } + + String fieldName = IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID; + String facetName = FACET_ASSIGNED_TO_ME; + + // Same as in super.stickyFacetBuilder + StickyFacetBuilder assignedToMeFacetBuilder = newStickyFacetBuilder(query, filters, queryBuilder); + BoolQueryBuilder facetFilter = assignedToMeFacetBuilder.getStickyFacetFilter(IS_ASSIGNED_FILTER, fieldName); + + FilterAggregationBuilder facetTopAggregation = AggregationBuilders + .filter(facetName + "__filter", facetFilter) + .subAggregation(addEffortAggregationIfNeeded(query, AggregationBuilders.terms(facetName + "__terms") + .field(fieldName) + .includeExclude(new IncludeExclude(escapeSpecialRegexChars(uuid), null)))); + + builder.addAggregation( + AggregationBuilders.global(facetName) + .subAggregation(facetTopAggregation)); + } + + public List listTags(@Nullable OrganizationDto organization, @Nullable String textQuery, int size) { + int maxPageSize = 500; + checkArgument(size <= maxPageSize, "Page size must be lower than or equals to " + maxPageSize); + if (size <= 0) { + return emptyList(); + } + + BoolQueryBuilder esQuery = boolQuery() + .filter(createAuthorizationFilter(true)); + if (organization != null) { + esQuery.filter(termQuery(FIELD_ISSUE_ORGANIZATION_UUID, organization.getUuid())); + } + + SearchRequestBuilder requestBuilder = client + .prepareSearch(INDEX_TYPE_ISSUE) + .setQuery(esQuery) + .setSize(0); + + TermsAggregationBuilder termsAggregation = AggregationBuilders.terms(AGGREGATION_NAME_FOR_TAGS) + .field(IssueIndexDefinition.FIELD_ISSUE_TAGS) + .size(size) + .order(Terms.Order.term(true)) + .minDocCount(1L); + if (textQuery != null) { + String escapedTextQuery = escapeSpecialRegexChars(textQuery); + termsAggregation.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapedTextQuery), null)); + } + requestBuilder.addAggregation(termsAggregation); + + SearchResponse searchResponse = requestBuilder.get(); + Terms issuesResult = searchResponse.getAggregations().get(AGGREGATION_NAME_FOR_TAGS); + return EsUtils.termsKeys(issuesResult); + } + + public Map countTags(IssueQuery query, int maxNumberOfTags) { + Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_TAGS, query, null, Terms.Order.count(false), maxNumberOfTags); + return EsUtils.termsToMap(terms); + } + + public List listAuthors(IssueQuery query, @Nullable String textQuery, int maxNumberOfAuthors) { + Terms terms = listTermsMatching(IssueIndexDefinition.FIELD_ISSUE_AUTHOR_LOGIN, query, textQuery, Terms.Order.term(true), maxNumberOfAuthors); + return EsUtils.termsKeys(terms); + } + + private Terms listTermsMatching(String fieldName, IssueQuery query, @Nullable String textQuery, Terms.Order termsOrder, int maxNumberOfTags) { + SearchRequestBuilder requestBuilder = client + .prepareSearch(INDEX_TYPE_ISSUE) + // Avoids returning search hits + .setSize(0); + + requestBuilder.setQuery(boolQuery().must(QueryBuilders.matchAllQuery()).filter(createBoolFilter(query))); + + TermsAggregationBuilder aggreg = AggregationBuilders.terms("_ref") + .field(fieldName) + .size(maxNumberOfTags) + .order(termsOrder) + .minDocCount(1L); + if (textQuery != null) { + aggreg.includeExclude(new IncludeExclude(format(SUBSTRING_MATCH_REGEXP, escapeSpecialRegexChars(textQuery)), null)); + } + + SearchResponse searchResponse = requestBuilder.addAggregation(aggreg).get(); + return searchResponse.getAggregations().get("_ref"); + } + + private BoolQueryBuilder createBoolFilter(IssueQuery query) { + BoolQueryBuilder boolQuery = boolQuery(); + for (QueryBuilder filter : createFilters(query).values()) { + if (filter != null) { + boolQuery.must(filter); + } + } + return boolQuery; + } + + public List searchProjectStatistics(List projectUuids, List froms, @Nullable String assigneeUuid) { + checkState(projectUuids.size() == froms.size(), + "Expected same size for projectUuids (had size %s) and froms (had size %s)", projectUuids.size(), froms.size()); + if (projectUuids.isEmpty()) { + return Collections.emptyList(); + } + SearchRequestBuilder request = client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) + .setQuery( + boolQuery() + .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)) + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_ASSIGNEE_UUID, assigneeUuid)) + .mustNot(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name()))) + .setSize(0); + IntStream.range(0, projectUuids.size()).forEach(i -> { + String projectUuid = projectUuids.get(i); + long from = froms.get(i); + request + .addAggregation(AggregationBuilders + .filter(projectUuid, boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_PROJECT_UUID, projectUuid)) + .filter(rangeQuery(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT).gte(epochMillisToEpochSeconds(from)))) + .subAggregation( + AggregationBuilders.terms("branchUuid").field(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID) + .subAggregation( + AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY)) + .subAggregation( + AggregationBuilders.max("maxFuncCreatedAt").field(IssueIndexDefinition.FIELD_ISSUE_FUNC_CREATED_AT)))); + }); + SearchResponse response = request.get(); + return response.getAggregations().asList().stream() + .map(x -> (InternalFilter) x) + .flatMap(projectBucket -> ((StringTerms) projectBucket.getAggregations().get("branchUuid")).getBuckets().stream() + .flatMap(branchBucket -> { + long count = ((InternalValueCount) branchBucket.getAggregations().get(COUNT)).getValue(); + if (count < 1L) { + return Stream.empty(); + } + long lastIssueDate = (long) ((InternalMax) branchBucket.getAggregations().get("maxFuncCreatedAt")).getValue(); + return Stream.of(new ProjectStatistics(branchBucket.getKeyAsString(), count, lastIssueDate)); + })) + .collect(MoreCollectors.toList(projectUuids.size())); + } + + public List searchBranchStatistics(String projectUuid, List branchUuids) { + if (branchUuids.isEmpty()) { + return Collections.emptyList(); + } + + SearchRequestBuilder request = client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) + .setRouting(projectUuid) + .setQuery( + boolQuery() + .must(termsQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, branchUuids)) + .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION)) + .must(termQuery(IssueIndexDefinition.FIELD_ISSUE_IS_MAIN_BRANCH, Boolean.toString(false)))) + .setSize(0) + .addAggregation(AggregationBuilders.terms("branchUuids") + .field(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID) + .size(branchUuids.size()) + .subAggregation(AggregationBuilders.terms("types") + .field(IssueIndexDefinition.FIELD_ISSUE_TYPE))); + SearchResponse response = request.get(); + return ((StringTerms) response.getAggregations().get("branchUuids")).getBuckets().stream() + .map(bucket -> new BranchStatistics(bucket.getKeyAsString(), + ((StringTerms) bucket.getAggregations().get("types")).getBuckets() + .stream() + .collect(uniqueIndex(StringTerms.Bucket::getKeyAsString, InternalTerms.Bucket::getDocCount)))) + .collect(MoreCollectors.toList(branchUuids.size())); + } + + public List getSansTop25Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) { + SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); + Stream.of(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES).forEach(sansCategory -> { + AggregationBuilder sansCategoryAggs = AggregationBuilders + .filter(sansCategory, boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_SANS_TOP_25, sansCategory))); + request.addAggregation(addSecurityReportSubAggregations(sansCategoryAggs, includeCwe)); + }); + return processSecurityReportSearchResults(request, includeCwe); + } + + public List getOwaspTop10Report(String projectUuid, boolean isViewOrApp, boolean includeCwe) { + SearchRequestBuilder request = prepareNonClosedVulnerabilitiesAndHotspotSearch(projectUuid, isViewOrApp); + Stream.concat(IntStream.rangeClosed(1, 10).mapToObj(i -> "a" + i), Stream.of(UNKNOWN_STANDARD)).forEach(owaspCategory -> { + AggregationBuilder owaspCategoryAggs = AggregationBuilders + .filter(owaspCategory, boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_OWASP_TOP_10, owaspCategory))); + request.addAggregation(addSecurityReportSubAggregations(owaspCategoryAggs, includeCwe)); + }); + return processSecurityReportSearchResults(request, includeCwe); + } + + private static List processSecurityReportSearchResults(SearchRequestBuilder request, boolean includeCwe) { + SearchResponse response = request.get(); + return response.getAggregations().asList().stream() + .map(c -> processSecurityReportIssueSearchResults((InternalFilter) c, includeCwe)) + .collect(MoreCollectors.toList()); + } + + private static SecurityStandardCategoryStatistics processSecurityReportIssueSearchResults(InternalFilter categoryBucket, boolean includeCwe) { + List children = new ArrayList<>(); + + if (includeCwe) { + ((StringTerms) categoryBucket.getAggregations().get("cwe")).getBuckets() + .forEach(cweBucket -> children.add(processSecurityReportCategorySearchResults(cweBucket, cweBucket.getKeyAsString(), null))); + } + + return processSecurityReportCategorySearchResults(categoryBucket, categoryBucket.getName(), children); + } + + private static SecurityStandardCategoryStatistics processSecurityReportCategorySearchResults(HasAggregations categoryBucket, String categoryName, + @Nullable List children) { + List severityBuckets = ((StringTerms) ((InternalFilter) categoryBucket.getAggregations().get("vulnerabilities")).getAggregations().get("severity")) + .getBuckets(); + long vulnerabilities = severityBuckets.stream().mapToLong(b -> ((InternalValueCount) b.getAggregations().get(COUNT)).getValue()).sum(); + // Worst severity having at least one issue + OptionalInt severityRating = severityBuckets.stream() + .filter(b -> ((InternalValueCount) b.getAggregations().get(COUNT)).getValue() != 0) + .mapToInt(b -> Severity.ALL.indexOf(b.getKeyAsString()) + 1) + .max(); + + long openSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("openSecurityHotspots")).getAggregations().get(COUNT)) + .getValue(); + long toReviewSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("toReviewSecurityHotspots")).getAggregations().get(COUNT)) + .getValue(); + long wontFixSecurityHotspots = ((InternalValueCount) ((InternalFilter) categoryBucket.getAggregations().get("wontFixSecurityHotspots")).getAggregations().get(COUNT)) + .getValue(); + + return new SecurityStandardCategoryStatistics(categoryName, vulnerabilities, severityRating, toReviewSecurityHotspots, openSecurityHotspots, + wontFixSecurityHotspots, children); + } + + private static AggregationBuilder addSecurityReportSubAggregations(AggregationBuilder categoriesAggs, boolean includeCwe) { + AggregationBuilder aggregationBuilder = addSecurityReportIssueCountAggregations(categoriesAggs); + if (includeCwe) { + categoriesAggs + .subAggregation(addSecurityReportIssueCountAggregations(AggregationBuilders.terms("cwe").field(IssueIndexDefinition.FIELD_ISSUE_CWE))); + } + return aggregationBuilder; + } + + private static AggregationBuilder addSecurityReportIssueCountAggregations(AggregationBuilder categoryAggs) { + return categoryAggs + .subAggregation( + AggregationBuilders.filter("vulnerabilities", boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.VULNERABILITY.name())) + .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))) + .subAggregation( + AggregationBuilders.terms("severity").field(IssueIndexDefinition.FIELD_ISSUE_SEVERITY) + .subAggregation( + AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY)))) + .subAggregation(AggregationBuilders.filter("openSecurityHotspots", boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .mustNot(existsQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION))) + .subAggregation( + AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))) + .subAggregation(AggregationBuilders.filter("toReviewSecurityHotspots", boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_FIXED))) + .subAggregation( + AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))) + .subAggregation(AggregationBuilders.filter("wontFixSecurityHotspots", boolQuery() + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name())) + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_RESOLVED)) + .filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_RESOLUTION, Issue.RESOLUTION_WONT_FIX))) + .subAggregation( + AggregationBuilders.count(COUNT).field(IssueIndexDefinition.FIELD_ISSUE_KEY))); + } + + private SearchRequestBuilder prepareNonClosedVulnerabilitiesAndHotspotSearch(String projectUuid, boolean isViewOrApp) { + BoolQueryBuilder componentFilter = boolQuery(); + if (isViewOrApp) { + componentFilter.filter(QueryBuilders.termsLookupQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, + new TermsLookup( + ViewIndexDefinition.INDEX_TYPE_VIEW.getIndex(), + ViewIndexDefinition.INDEX_TYPE_VIEW.getType(), + projectUuid, + ViewIndexDefinition.FIELD_PROJECTS))); + } else { + componentFilter.filter(termQuery(IssueIndexDefinition.FIELD_ISSUE_BRANCH_UUID, projectUuid)); + } + return client.prepareSearch(IssueIndexDefinition.INDEX_TYPE_ISSUE) + .setQuery( + componentFilter + .filter(termsQuery(IssueIndexDefinition.FIELD_ISSUE_TYPE, RuleType.SECURITY_HOTSPOT.name(), RuleType.VULNERABILITY.name())) + .mustNot(termQuery(IssueIndexDefinition.FIELD_ISSUE_STATUS, Issue.STATUS_CLOSED))) + .setSize(0); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQuery.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQuery.java new file mode 100644 index 00000000000..dfb60a7a22b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQuery.java @@ -0,0 +1,553 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.builder.ReflectionToStringBuilder; +import org.sonar.db.rule.RuleDefinitionDto; + +import static com.google.common.base.Preconditions.checkArgument; +import static org.sonar.server.es.SearchOptions.MAX_LIMIT; + +/** + * @since 3.6 + */ +public class IssueQuery { + + public static final String SORT_BY_CREATION_DATE = "CREATION_DATE"; + public static final String SORT_BY_UPDATE_DATE = "UPDATE_DATE"; + public static final String SORT_BY_CLOSE_DATE = "CLOSE_DATE"; + /** + * @deprecated since 7.2, it's no more possible to sort by assignee + */ + @Deprecated + public static final String SORT_BY_ASSIGNEE = "ASSIGNEE"; + public static final String SORT_BY_SEVERITY = "SEVERITY"; + public static final String SORT_BY_STATUS = "STATUS"; + + /** + * Sort by project, file path then line id + */ + public static final String SORT_BY_FILE_LINE = "FILE_LINE"; + + public static final Set SORTS = ImmutableSet.of(SORT_BY_CREATION_DATE, SORT_BY_UPDATE_DATE, SORT_BY_CLOSE_DATE, SORT_BY_ASSIGNEE, SORT_BY_SEVERITY, + SORT_BY_STATUS, SORT_BY_FILE_LINE); + + private final Collection issueKeys; + private final Collection severities; + private final Collection statuses; + private final Collection resolutions; + private final Collection components; + private final Collection modules; + private final Collection moduleRoots; + private final Collection projects; + private final Collection directories; + private final Collection files; + private final Collection views; + private final Collection rules; + private final Collection assignees; + private final Collection authors; + private final Collection languages; + private final Collection tags; + private final Collection types; + private final Collection owaspTop10; + private final Collection sansTop25; + private final Collection cwe; + private final Map createdAfterByProjectUuids; + private final Boolean onComponentOnly; + private final Boolean assigned; + private final Boolean resolved; + private final Date createdAt; + private final PeriodStart createdAfter; + private final Date createdBefore; + private final String sort; + private final Boolean asc; + private final String facetMode; + private final String organizationUuid; + private final String branchUuid; + private final boolean mainBranch; + private final boolean checkAuthorization; + + private IssueQuery(Builder builder) { + this.issueKeys = defaultCollection(builder.issueKeys); + this.severities = defaultCollection(builder.severities); + this.statuses = defaultCollection(builder.statuses); + this.resolutions = defaultCollection(builder.resolutions); + this.components = defaultCollection(builder.components); + this.modules = defaultCollection(builder.modules); + this.moduleRoots = defaultCollection(builder.moduleRoots); + this.projects = defaultCollection(builder.projects); + this.directories = defaultCollection(builder.directories); + this.files = defaultCollection(builder.files); + this.views = defaultCollection(builder.views); + this.rules = defaultCollection(builder.rules); + this.assignees = defaultCollection(builder.assigneeUuids); + this.authors = defaultCollection(builder.authors); + this.languages = defaultCollection(builder.languages); + this.tags = defaultCollection(builder.tags); + this.types = defaultCollection(builder.types); + this.owaspTop10 = defaultCollection(builder.owaspTop10); + this.sansTop25 = defaultCollection(builder.sansTop25); + this.cwe = defaultCollection(builder.cwe); + this.createdAfterByProjectUuids = defaultMap(builder.createdAfterByProjectUuids); + this.onComponentOnly = builder.onComponentOnly; + this.assigned = builder.assigned; + this.resolved = builder.resolved; + this.createdAt = builder.createdAt; + this.createdAfter = builder.createdAfter; + this.createdBefore = builder.createdBefore; + this.sort = builder.sort; + this.asc = builder.asc; + this.checkAuthorization = builder.checkAuthorization; + this.facetMode = builder.facetMode; + this.organizationUuid = builder.organizationUuid; + this.branchUuid = builder.branchUuid; + this.mainBranch = builder.mainBranch; + } + + public Collection issueKeys() { + return issueKeys; + } + + public Collection severities() { + return severities; + } + + public Collection statuses() { + return statuses; + } + + public Collection resolutions() { + return resolutions; + } + + public Collection componentUuids() { + return components; + } + + public Collection moduleUuids() { + return modules; + } + + public Collection moduleRootUuids() { + return moduleRoots; + } + + public Collection projectUuids() { + return projects; + } + + public Collection directories() { + return directories; + } + + public Collection fileUuids() { + return files; + } + + public Collection viewUuids() { + return views; + } + + public Collection rules() { + return rules; + } + + public Collection assignees() { + return assignees; + } + + public Collection authors() { + return authors; + } + + public Collection languages() { + return languages; + } + + public Collection tags() { + return tags; + } + + public Collection types() { + return types; + } + + public Collection owaspTop10() { + return owaspTop10; + } + + public Collection sansTop25() { + return sansTop25; + } + + public Collection cwe() { + return cwe; + } + + public Map createdAfterByProjectUuids() { + return createdAfterByProjectUuids; + } + + @CheckForNull + public Boolean onComponentOnly() { + return onComponentOnly; + } + + @CheckForNull + public Boolean assigned() { + return assigned; + } + + @CheckForNull + public Boolean resolved() { + return resolved; + } + + @CheckForNull + public PeriodStart createdAfter() { + return createdAfter; + } + + @CheckForNull + public Date createdAt() { + return createdAt == null ? null : new Date(createdAt.getTime()); + } + + @CheckForNull + public Date createdBefore() { + return createdBefore == null ? null : new Date(createdBefore.getTime()); + } + + @CheckForNull + public String sort() { + return sort; + } + + @CheckForNull + public Boolean asc() { + return asc; + } + + public boolean checkAuthorization() { + return checkAuthorization; + } + + @CheckForNull + public String organizationUuid() { + return organizationUuid; + } + + @CheckForNull + public String branchUuid() { + return branchUuid; + } + + public boolean isMainBranch() { + return mainBranch; + } + + public String facetMode() { + return facetMode; + } + + @Override + public String toString() { + return ReflectionToStringBuilder.toString(this); + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Collection issueKeys; + private Collection severities; + private Collection statuses; + private Collection resolutions; + private Collection components; + private Collection modules; + private Collection moduleRoots; + private Collection projects; + private Collection directories; + private Collection files; + private Collection views; + private Collection rules; + private Collection assigneeUuids; + private Collection authors; + private Collection languages; + private Collection tags; + private Collection types; + private Collection owaspTop10; + private Collection sansTop25; + private Collection cwe; + private Map createdAfterByProjectUuids; + private Boolean onComponentOnly = false; + private Boolean assigned = null; + private Boolean resolved = null; + private Date createdAt; + private PeriodStart createdAfter; + private Date createdBefore; + private String sort; + private Boolean asc = false; + private boolean checkAuthorization = true; + private String facetMode; + private String organizationUuid; + private String branchUuid; + private boolean mainBranch = true; + + private Builder() { + + } + + public Builder issueKeys(@Nullable Collection l) { + this.issueKeys = l; + return this; + } + + public Builder severities(@Nullable Collection l) { + this.severities = l; + return this; + } + + public Builder statuses(@Nullable Collection l) { + this.statuses = l; + return this; + } + + public Builder resolutions(@Nullable Collection l) { + this.resolutions = l; + return this; + } + + public Builder componentUuids(@Nullable Collection l) { + this.components = l; + return this; + } + + public Builder moduleUuids(@Nullable Collection l) { + this.modules = l; + return this; + } + + public Builder moduleRootUuids(@Nullable Collection l) { + this.moduleRoots = l; + return this; + } + + public Builder projectUuids(@Nullable Collection l) { + this.projects = l; + return this; + } + + public Builder directories(@Nullable Collection l) { + this.directories = l; + return this; + } + + public Builder fileUuids(@Nullable Collection l) { + this.files = l; + return this; + } + + public Builder viewUuids(@Nullable Collection l) { + this.views = l; + return this; + } + + public Builder rules(@Nullable Collection rules) { + this.rules = rules; + return this; + } + + public Builder assigneeUuids(@Nullable Collection l) { + this.assigneeUuids = l; + return this; + } + + public Builder authors(@Nullable Collection l) { + this.authors = l; + return this; + } + + public Builder languages(@Nullable Collection l) { + this.languages = l; + return this; + } + + public Builder tags(@Nullable Collection t) { + this.tags = t; + return this; + } + + public Builder types(@Nullable Collection t) { + this.types = t; + return this; + } + + public Builder owaspTop10(@Nullable Collection o) { + this.owaspTop10 = o; + return this; + } + + public Builder sansTop25(@Nullable Collection s) { + this.sansTop25 = s; + return this; + } + + public Builder cwe(@Nullable Collection cwe) { + this.cwe = cwe; + return this; + } + + public Builder createdAfterByProjectUuids(@Nullable Map createdAfterByProjectUuids) { + this.createdAfterByProjectUuids = createdAfterByProjectUuids; + return this; + } + + /** + * If true, it will return only issues on the passed component(s) + * If false, it will return all issues on the passed component(s) and their descendants + */ + public Builder onComponentOnly(@Nullable Boolean b) { + this.onComponentOnly = b; + return this; + } + + /** + * If true, it will return all issues assigned to someone + * If false, it will return all issues not assigned to someone + */ + public Builder assigned(@Nullable Boolean b) { + this.assigned = b; + return this; + } + + /** + * If true, it will return all resolved issues + * If false, it will return all none resolved issues + */ + public Builder resolved(@Nullable Boolean resolved) { + this.resolved = resolved; + return this; + } + + public Builder createdAt(@Nullable Date d) { + this.createdAt = d == null ? null : new Date(d.getTime()); + return this; + } + + public Builder createdAfter(@Nullable Date d) { + this.createdAfter(d, true); + return this; + } + + public Builder createdAfter(@Nullable Date d, boolean inclusive) { + this.createdAfter = d == null ? null : new PeriodStart(new Date(d.getTime()), inclusive); + return this; + } + + public Builder createdBefore(@Nullable Date d) { + this.createdBefore = d == null ? null : new Date(d.getTime()); + return this; + } + + public Builder sort(@Nullable String s) { + if (s != null && !SORTS.contains(s)) { + throw new IllegalArgumentException("Bad sort field: " + s); + } + this.sort = s; + return this; + } + + public Builder asc(@Nullable Boolean asc) { + this.asc = asc; + return this; + } + + public IssueQuery build() { + if (issueKeys != null) { + checkArgument(issueKeys.size() <= MAX_LIMIT, "Number of issue keys must be less than " + MAX_LIMIT + " (got " + issueKeys.size() + ")"); + } + return new IssueQuery(this); + } + + public Builder checkAuthorization(boolean checkAuthorization) { + this.checkAuthorization = checkAuthorization; + return this; + } + + public Builder facetMode(String facetMode) { + this.facetMode = facetMode; + return this; + } + + public Builder organizationUuid(String s) { + this.organizationUuid = s; + return this; + } + + public Builder branchUuid(@Nullable String s) { + this.branchUuid = s; + return this; + } + + public Builder mainBranch(boolean mainBranch) { + this.mainBranch = mainBranch; + return this; + } + } + + private static Collection defaultCollection(@Nullable Collection c) { + return c == null ? Collections.emptyList() : Collections.unmodifiableCollection(c); + } + + private static Map defaultMap(@Nullable Map map) { + return map == null ? Collections.emptyMap() : Collections.unmodifiableMap(map); + } + + public static class PeriodStart { + private final Date date; + private final boolean inclusive; + + public PeriodStart(Date date, boolean inclusive) { + this.date = date; + this.inclusive = inclusive; + + } + + public Date date() { + return date; + } + + public boolean inclusive() { + return inclusive; + } + + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java new file mode 100644 index 00000000000..5e89f78cb70 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/IssueQueryFactory.java @@ -0,0 +1,373 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import java.time.Clock; +import java.time.OffsetDateTime; +import java.time.Period; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.BooleanUtils; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.server.ServerSide; +import org.sonar.api.web.UserRole; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.server.issue.SearchRequest; +import org.sonar.server.issue.index.IssueQuery.PeriodStart; +import org.sonar.server.user.UserSession; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Collections2.transform; +import static java.lang.String.format; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.sonar.api.utils.DateUtils.longToDate; +import static org.sonar.api.utils.DateUtils.parseDateOrDateTime; +import static org.sonar.api.utils.DateUtils.parseEndingDateOrDateTime; +import static org.sonar.api.utils.DateUtils.parseStartingDateOrDateTime; +import static org.sonar.core.util.stream.MoreCollectors.toHashSet; +import static org.sonar.core.util.stream.MoreCollectors.toList; +import static org.sonar.core.util.stream.MoreCollectors.toSet; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_ROOTS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_UUIDS; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_AFTER; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_CREATED_IN_LAST; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD; + +/** + * This component is used to create an IssueQuery, in order to transform the component and component roots keys into uuid. + */ +@ServerSide +public class IssueQueryFactory { + + public static final String UNKNOWN = ""; + private static final ComponentDto UNKNOWN_COMPONENT = new ComponentDto().setUuid(UNKNOWN).setProjectUuid(UNKNOWN); + + private final DbClient dbClient; + private final Clock clock; + private final UserSession userSession; + + public IssueQueryFactory(DbClient dbClient, Clock clock, UserSession userSession) { + this.dbClient = dbClient; + this.clock = clock; + this.userSession = userSession; + } + + public IssueQuery create(SearchRequest request) { + try (DbSession dbSession = dbClient.openSession(false)) { + IssueQuery.Builder builder = IssueQuery.builder() + .issueKeys(request.getIssues()) + .severities(request.getSeverities()) + .statuses(request.getStatuses()) + .resolutions(request.getResolutions()) + .resolved(request.getResolved()) + .rules(ruleKeysToRuleId(dbSession, request.getRules())) + .assigneeUuids(request.getAssigneeUuids()) + .languages(request.getLanguages()) + .tags(request.getTags()) + .types(request.getTypes()) + .owaspTop10(request.getOwaspTop10()) + .sansTop25(request.getSansTop25()) + .cwe(request.getCwe()) + .assigned(request.getAssigned()) + .createdAt(parseDateOrDateTime(request.getCreatedAt())) + .createdBefore(parseEndingDateOrDateTime(request.getCreatedBefore())) + .facetMode(request.getFacetMode()) + .organizationUuid(convertOrganizationKeyToUuid(dbSession, request.getOrganization())); + + List allComponents = new ArrayList<>(); + boolean effectiveOnComponentOnly = mergeDeprecatedComponentParameters(dbSession, request, allComponents); + addComponentParameters(builder, dbSession, effectiveOnComponentOnly, allComponents, request); + + setCreatedAfterFromRequest(dbSession, builder, request, allComponents); + String sort = request.getSort(); + if (!Strings.isNullOrEmpty(sort)) { + builder.sort(sort); + builder.asc(request.getAsc()); + } + return builder.build(); + } + } + + private void setCreatedAfterFromDates(IssueQuery.Builder builder, @Nullable Date createdAfter, @Nullable String createdInLast, boolean createdAfterInclusive) { + checkArgument(createdAfter == null || createdInLast == null, format("Parameters %s and %s cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_CREATED_IN_LAST)); + + Date actualCreatedAfter = createdAfter; + if (createdInLast != null) { + actualCreatedAfter = Date.from( + OffsetDateTime.now(clock) + .minus(Period.parse("P" + createdInLast.toUpperCase(Locale.ENGLISH))) + .toInstant()); + } + builder.createdAfter(actualCreatedAfter, createdAfterInclusive); + } + + @CheckForNull + private String convertOrganizationKeyToUuid(DbSession dbSession, @Nullable String organizationKey) { + if (organizationKey == null) { + return null; + } + Optional organization = dbClient.organizationDao().selectByKey(dbSession, organizationKey); + return organization.map(OrganizationDto::getUuid).orElse(UNKNOWN); + } + + private void setCreatedAfterFromRequest(DbSession dbSession, IssueQuery.Builder builder, SearchRequest request, List componentUuids) { + Date createdAfter = parseStartingDateOrDateTime(request.getCreatedAfter()); + String createdInLast = request.getCreatedInLast(); + + if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) { + setCreatedAfterFromDates(builder, createdAfter, createdInLast, true); + } else { + checkArgument(createdAfter == null, "Parameters '%s' and '%s' cannot be set simultaneously", PARAM_CREATED_AFTER, PARAM_SINCE_LEAK_PERIOD); + checkArgument(componentUuids.size() == 1, "One and only one component must be provided when searching since leak period"); + ComponentDto component = componentUuids.iterator().next(); + Date createdAfterFromSnapshot = findCreatedAfterFromComponentUuid(dbSession, component); + setCreatedAfterFromDates(builder, createdAfterFromSnapshot, createdInLast, false); + } + } + + @CheckForNull + private Date findCreatedAfterFromComponentUuid(DbSession dbSession, ComponentDto component) { + Optional snapshot = dbClient.snapshotDao().selectLastAnalysisByComponentUuid(dbSession, component.uuid()); + return snapshot.map(s -> longToDate(s.getPeriodDate())).orElse(null); + } + + private boolean mergeDeprecatedComponentParameters(DbSession session, SearchRequest request, List allComponents) { + Boolean onComponentOnly = request.getOnComponentOnly(); + Collection components = request.getComponents(); + Collection componentUuids = request.getComponentUuids(); + Collection componentKeys = request.getComponentKeys(); + Collection componentRootUuids = request.getComponentRootUuids(); + Collection componentRoots = request.getComponentRoots(); + String branch = request.getBranch(); + String pullRequest = request.getPullRequest(); + + boolean effectiveOnComponentOnly = false; + + checkArgument(atMostOneNonNullElement(components, componentUuids, componentKeys, componentRootUuids, componentRoots), + "At most one of the following parameters can be provided: %s, %s, %s, %s, %s", + PARAM_COMPONENT_KEYS, PARAM_COMPONENT_UUIDS, PARAM_COMPONENTS, PARAM_COMPONENT_ROOTS, PARAM_COMPONENT_UUIDS); + + if (componentRootUuids != null) { + allComponents.addAll(getComponentsFromUuids(session, componentRootUuids)); + } else if (componentRoots != null) { + allComponents.addAll(getComponentsFromKeys(session, componentRoots, branch, pullRequest)); + } else if (components != null) { + allComponents.addAll(getComponentsFromKeys(session, components, branch, pullRequest)); + effectiveOnComponentOnly = true; + } else if (componentUuids != null) { + allComponents.addAll(getComponentsFromUuids(session, componentUuids)); + effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly); + } else if (componentKeys != null) { + allComponents.addAll(getComponentsFromKeys(session, componentKeys, branch, pullRequest)); + effectiveOnComponentOnly = BooleanUtils.isTrue(onComponentOnly); + } + + return effectiveOnComponentOnly; + } + + private static boolean atMostOneNonNullElement(Object... objects) { + return Arrays.stream(objects) + .filter(Objects::nonNull) + .count() <= 1; + } + + private void addComponentParameters(IssueQuery.Builder builder, DbSession session, boolean onComponentOnly, List components, SearchRequest request) { + builder.onComponentOnly(onComponentOnly); + if (onComponentOnly) { + builder.componentUuids(components.stream().map(ComponentDto::uuid).collect(toList())); + setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest()); + return; + } + + builder.authors(request.getAuthors()); + List projectUuids = request.getProjectUuids(); + List projectKeys = request.getProjectKeys(); + checkArgument(projectUuids == null || projectKeys == null, "Parameters projects and projectUuids cannot be set simultaneously"); + if (projectUuids != null) { + builder.projectUuids(projectUuids); + } else if (projectKeys != null) { + List projects = getComponentsFromKeys(session, projectKeys, request.getBranch(), request.getPullRequest()); + builder.projectUuids(projects.stream().map(IssueQueryFactory::toProjectUuid).collect(toList())); + setBranch(builder, projects.get(0), request.getBranch(), request.getPullRequest()); + } + builder.moduleUuids(request.getModuleUuids()); + builder.directories(request.getDirectories()); + builder.fileUuids(request.getFileUuids()); + + addComponentsBasedOnQualifier(builder, session, components, request); + } + + private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, List components, SearchRequest request) { + if (components.isEmpty()) { + return; + } + if (components.stream().map(ComponentDto::uuid).anyMatch(uuid -> uuid.equals(UNKNOWN))) { + builder.componentUuids(singleton(UNKNOWN)); + return; + } + + Set qualifiers = components.stream().map(ComponentDto::qualifier).collect(toHashSet()); + checkArgument(qualifiers.size() == 1, "All components must have the same qualifier, found %s", String.join(",", qualifiers)); + + setBranch(builder, components.get(0), request.getBranch(), request.getPullRequest()); + String qualifier = qualifiers.iterator().next(); + switch (qualifier) { + case Qualifiers.VIEW: + case Qualifiers.SUBVIEW: + addViewsOrSubViews(builder, components); + break; + case Qualifiers.APP: + addApplications(builder, dbSession, components, request); + break; + case Qualifiers.PROJECT: + builder.projectUuids(components.stream().map(IssueQueryFactory::toProjectUuid).collect(toList())); + break; + case Qualifiers.MODULE: + builder.moduleRootUuids(components.stream().map(ComponentDto::uuid).collect(toList())); + break; + case Qualifiers.DIRECTORY: + addDirectories(builder, components); + break; + case Qualifiers.FILE: + case Qualifiers.UNIT_TEST_FILE: + builder.fileUuids(components.stream().map(ComponentDto::uuid).collect(toList())); + break; + default: + throw new IllegalArgumentException("Unable to set search root context for components " + Joiner.on(',').join(components)); + } + } + + private void addViewsOrSubViews(IssueQuery.Builder builder, Collection viewOrSubViewUuids) { + List filteredViewUuids = viewOrSubViewUuids.stream() + .filter(uuid -> userSession.hasComponentPermission(UserRole.USER, uuid)) + .map(ComponentDto::uuid) + .collect(Collectors.toList()); + if (filteredViewUuids.isEmpty()) { + filteredViewUuids.add(UNKNOWN); + } + builder.viewUuids(filteredViewUuids); + } + + private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List applications, SearchRequest request) { + Set authorizedApplicationUuids = applications.stream() + .filter(app -> userSession.hasComponentPermission(UserRole.USER, app)) + .map(ComponentDto::uuid) + .collect(toSet()); + + builder.viewUuids(authorizedApplicationUuids.isEmpty() ? singleton(UNKNOWN) : authorizedApplicationUuids); + addCreatedAfterByProjects(builder, dbSession, request, authorizedApplicationUuids); + } + + private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchRequest request, Set applicationUuids) { + if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) { + return; + } + + Set projectUuids = applicationUuids.stream() + .flatMap(app -> dbClient.componentDao().selectProjectsFromView(dbSession, app, app).stream()) + .collect(toSet()); + + Map leakByProjects = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids) + .stream() + .filter(s -> s.getPeriodDate() != null) + .collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> new PeriodStart(longToDate(s.getPeriodDate()), false))); + builder.createdAfterByProjectUuids(leakByProjects); + } + + private static void addDirectories(IssueQuery.Builder builder, List directories) { + Collection directoryModuleUuids = new HashSet<>(); + Collection directoryPaths = new HashSet<>(); + for (ComponentDto directory : directories) { + directoryModuleUuids.add(directory.moduleUuid()); + directoryPaths.add(directory.path()); + } + builder.moduleUuids(directoryModuleUuids); + builder.directories(directoryPaths); + } + + private List getComponentsFromKeys(DbSession dbSession, Collection componentKeys, @Nullable String branch, @Nullable String pullRequest) { + List componentDtos; + if (branch != null) { + componentDtos = dbClient.componentDao().selectByKeysAndBranch(dbSession, componentKeys, branch); + } else if (pullRequest != null) { + componentDtos = dbClient.componentDao().selectByKeysAndPullRequest(dbSession, componentKeys, pullRequest); + } else { + componentDtos = dbClient.componentDao().selectByKeys(dbSession, componentKeys); + } + if (!componentKeys.isEmpty() && componentDtos.isEmpty()) { + return singletonList(UNKNOWN_COMPONENT); + } + return componentDtos; + } + + private List getComponentsFromUuids(DbSession dbSession, Collection componentUuids) { + List componentDtos = dbClient.componentDao().selectByUuids(dbSession, componentUuids); + if (!componentUuids.isEmpty() && componentDtos.isEmpty()) { + return singletonList(UNKNOWN_COMPONENT); + } + return componentDtos; + } + + @CheckForNull + private Collection ruleKeysToRuleId(DbSession dbSession, @Nullable Collection rules) { + if (rules != null) { + return dbClient.ruleDao().selectDefinitionByKeys(dbSession, transform(rules, RuleKey::parse)); + } + return Collections.emptyList(); + } + + private static String toProjectUuid(ComponentDto componentDto) { + String mainBranchProjectUuid = componentDto.getMainBranchProjectUuid(); + return mainBranchProjectUuid == null ? componentDto.projectUuid() : mainBranchProjectUuid; + } + + private static void setBranch(IssueQuery.Builder builder, ComponentDto component, @Nullable String branch, @Nullable String pullRequest) { + builder.branchUuid(branch == null && pullRequest == null ? null : component.projectUuid()); + builder.mainBranch(UNKNOWN_COMPONENT.equals(component) + || (branch == null && pullRequest == null) + || (branch != null && !branch.equals(component.getBranch())) + || (pullRequest != null && !pullRequest.equals(component.getPullRequest()))); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/index/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/issue/index/package-info.java new file mode 100644 index 00000000000..c4487cb828b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/index/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java index 27fb50a7a6e..0f22690ef2f 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/AuthorsAction.java @@ -28,7 +28,7 @@ import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.api.server.ws.WebService.Param; import org.sonar.api.utils.text.JsonWriter; -import org.sonar.server.issue.IssueQuery; +import org.sonar.server.issue.index.IssueQuery; import org.sonar.server.issue.index.IssueIndex; import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_AUTHORS; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java index a63cb6b72cf..eab940d4adf 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/ComponentTagsAction.java @@ -26,8 +26,8 @@ import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; import org.sonar.api.server.ws.WebService.NewAction; import org.sonar.api.utils.text.JsonWriter; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.SearchRequest; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java index 981f13099c6..19a817c45b5 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/IssueWsModule.java @@ -22,7 +22,7 @@ package org.sonar.server.issue.ws; import org.sonar.core.platform.Module; import org.sonar.server.issue.IssueFieldsSetter; import org.sonar.server.issue.IssueFinder; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.WebIssueStorage; import org.sonar.server.issue.IssueUpdater; import org.sonar.server.issue.TransitionService; diff --git a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java index a2cbf329ebc..0b03a534e52 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java @@ -39,7 +39,6 @@ import org.elasticsearch.search.SearchHit; import org.sonar.api.issue.Issue; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; -import org.sonar.api.rules.Rule; import org.sonar.api.rules.RuleType; import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; @@ -57,8 +56,8 @@ import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.db.user.UserDto; import org.sonar.server.es.Facets; import org.sonar.server.es.SearchOptions; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.SearchRequest; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.user.UserSession; @@ -76,12 +75,12 @@ import static java.util.stream.Collectors.toList; import static org.sonar.api.utils.Paging.forPageIndex; import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.server.es.SearchOptions.MAX_LIMIT; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_INSECURE_INTERACTION; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_POROUS_DEFENSES; -import static org.sonar.server.issue.IssueQuery.SANS_TOP_25_RISKY_RESOURCE; -import static org.sonar.server.issue.IssueQuery.SORT_BY_ASSIGNEE; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; -import static org.sonar.server.issue.IssueQueryFactory.UNKNOWN; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_INSECURE_INTERACTION; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_POROUS_DEFENSES; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_RISKY_RESOURCE; +import static org.sonar.server.issue.index.IssueQuery.SORT_BY_ASSIGNEE; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; +import static org.sonar.server.issue.index.IssueQueryFactory.UNKNOWN; 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; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java new file mode 100644 index 00000000000..989956a0569 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresIndex.java @@ -0,0 +1,450 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import javax.annotation.Nullable; +import org.apache.lucene.search.join.ScoreMode; +import org.elasticsearch.action.search.SearchRequestBuilder; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilders; +import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation.Bucket; +import org.elasticsearch.search.aggregations.bucket.filter.Filter; +import org.elasticsearch.search.aggregations.bucket.filters.FiltersAggregator.KeyedFilter; +import org.elasticsearch.search.aggregations.bucket.nested.Nested; +import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.Terms; +import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.elasticsearch.search.aggregations.bucket.terms.support.IncludeExclude; +import org.elasticsearch.search.aggregations.metrics.sum.Sum; +import org.elasticsearch.search.sort.FieldSortBuilder; +import org.sonar.api.server.ServerSide; +import org.sonar.api.utils.System2; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.server.es.DefaultIndexSettingsElement; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.SearchIdResult; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.es.StickyFacetBuilder; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Collections.emptyList; +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.nestedQuery; +import static org.elasticsearch.index.query.QueryBuilders.rangeQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.elasticsearch.index.query.QueryBuilders.termsQuery; +import static org.elasticsearch.search.aggregations.AggregationBuilders.filters; +import static org.elasticsearch.search.aggregations.AggregationBuilders.sum; +import static org.elasticsearch.search.sort.SortOrder.ASC; +import static org.elasticsearch.search.sort.SortOrder.DESC; +import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; +import static org.sonar.api.measures.CoreMetrics.DUPLICATED_LINES_DENSITY_KEY; +import static org.sonar.api.measures.CoreMetrics.NCLOC_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_COVERAGE_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_LINES_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY; +import static org.sonar.server.es.EsUtils.escapeSpecialRegexChars; +import static org.sonar.server.es.EsUtils.termsToMap; +import static org.sonar.server.measure.index.ProjectMeasuresDoc.QUALITY_GATE_STATUS; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ANALYSED_AT; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_LANGUAGES; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NCLOC_LANGUAGE_DISTRIBUTION; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_ORGANIZATION_UUID; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_QUALITY_GATE_STATUS; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_TAGS; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES; +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_TAGS; +import static org.sonarqube.ws.client.project.ProjectsWsParameters.MAX_PAGE_SIZE; + +@ServerSide +public class ProjectMeasuresIndex { + + public static final List SUPPORTED_FACETS = ImmutableList.of( + NCLOC_KEY, + NEW_LINES_KEY, + DUPLICATED_LINES_DENSITY_KEY, + NEW_DUPLICATED_LINES_DENSITY_KEY, + COVERAGE_KEY, + NEW_COVERAGE_KEY, + SQALE_RATING_KEY, + NEW_MAINTAINABILITY_RATING_KEY, + RELIABILITY_RATING_KEY, + NEW_RELIABILITY_RATING_KEY, + SECURITY_RATING_KEY, + NEW_SECURITY_RATING_KEY, + ALERT_STATUS_KEY, + FILTER_LANGUAGES, + FILTER_TAGS); + + private static final Double[] LINES_THRESHOLDS = new Double[] {1_000d, 10_000d, 100_000d, 500_000d}; + private static final Double[] COVERAGE_THRESHOLDS = new Double[] {30d, 50d, 70d, 80d}; + private static final Double[] DUPLICATIONS_THRESHOLDS = new Double[] {3d, 5d, 10d, 20d}; + + private static final String FIELD_MEASURES_KEY = FIELD_MEASURES + "." + ProjectMeasuresIndexDefinition.FIELD_MEASURES_KEY; + private static final String FIELD_MEASURES_VALUE = FIELD_MEASURES + "." + ProjectMeasuresIndexDefinition.FIELD_MEASURES_VALUE; + private static final String FIELD_DISTRIB_LANGUAGE = FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "." + ProjectMeasuresIndexDefinition.FIELD_DISTRIB_LANGUAGE; + private static final String FIELD_DISTRIB_NCLOC = FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "." + ProjectMeasuresIndexDefinition.FIELD_DISTRIB_NCLOC; + + private static final Map FACET_FACTORIES = ImmutableMap.builder() + .put(NCLOC_KEY, (esSearch, query, facetBuilder) -> addRangeFacet(esSearch, NCLOC_KEY, facetBuilder, LINES_THRESHOLDS)) + .put(NEW_LINES_KEY, (esSearch, query, facetBuilder) -> addRangeFacet(esSearch, NEW_LINES_KEY, facetBuilder, LINES_THRESHOLDS)) + .put(DUPLICATED_LINES_DENSITY_KEY, + (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, DUPLICATED_LINES_DENSITY_KEY, facetBuilder, DUPLICATIONS_THRESHOLDS)) + .put(NEW_DUPLICATED_LINES_DENSITY_KEY, + (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, NEW_DUPLICATED_LINES_DENSITY_KEY, facetBuilder, DUPLICATIONS_THRESHOLDS)) + .put(COVERAGE_KEY, (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, COVERAGE_KEY, facetBuilder, COVERAGE_THRESHOLDS)) + .put(NEW_COVERAGE_KEY, (esSearch, query, facetBuilder) -> addRangeFacetIncludingNoData(esSearch, NEW_COVERAGE_KEY, facetBuilder, COVERAGE_THRESHOLDS)) + .put(SQALE_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, SQALE_RATING_KEY, facetBuilder)) + .put(NEW_MAINTAINABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_MAINTAINABILITY_RATING_KEY, facetBuilder)) + .put(RELIABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, RELIABILITY_RATING_KEY, facetBuilder)) + .put(NEW_RELIABILITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_RELIABILITY_RATING_KEY, facetBuilder)) + .put(SECURITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, SECURITY_RATING_KEY, facetBuilder)) + .put(NEW_SECURITY_RATING_KEY, (esSearch, query, facetBuilder) -> addRatingFacet(esSearch, NEW_SECURITY_RATING_KEY, facetBuilder)) + .put(ALERT_STATUS_KEY, (esSearch, query, facetBuilder) -> esSearch.addAggregation(createStickyFacet(ALERT_STATUS_KEY, facetBuilder, createQualityGateFacet()))) + .put(FILTER_LANGUAGES, ProjectMeasuresIndex::addLanguagesFacet) + .put(FIELD_TAGS, ProjectMeasuresIndex::addTagsFacet) + .build(); + + private final EsClient client; + private final WebAuthorizationTypeSupport authorizationTypeSupport; + private final System2 system2; + + public ProjectMeasuresIndex(EsClient client, WebAuthorizationTypeSupport authorizationTypeSupport, System2 system2) { + this.client = client; + this.authorizationTypeSupport = authorizationTypeSupport; + this.system2 = system2; + } + + public SearchIdResult search(ProjectMeasuresQuery query, SearchOptions searchOptions) { + SearchRequestBuilder requestBuilder = client + .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) + .setFetchSource(false) + .setFrom(searchOptions.getOffset()) + .setSize(searchOptions.getLimit()); + + BoolQueryBuilder esFilter = boolQuery(); + Map filters = createFilters(query); + filters.values().forEach(esFilter::must); + requestBuilder.setQuery(esFilter); + + addFacets(requestBuilder, searchOptions, filters, query); + addSort(query, requestBuilder); + return new SearchIdResult<>(requestBuilder.get(), id -> id, system2.getDefaultTimeZone()); + } + + public ProjectMeasuresStatistics searchTelemetryStatistics() { + SearchRequestBuilder request = client + .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) + .setFetchSource(false) + .setSize(0); + + BoolQueryBuilder esFilter = boolQuery(); + request.setQuery(esFilter); + request.addAggregation(AggregationBuilders.terms(FIELD_LANGUAGES) + .field(FIELD_LANGUAGES) + .size(MAX_PAGE_SIZE) + .minDocCount(1) + .order(Terms.Order.count(false))); + request.addAggregation(AggregationBuilders.nested(FIELD_NCLOC_LANGUAGE_DISTRIBUTION, FIELD_NCLOC_LANGUAGE_DISTRIBUTION) + .subAggregation(AggregationBuilders.terms(FIELD_NCLOC_LANGUAGE_DISTRIBUTION + "_terms") + .field(FIELD_DISTRIB_LANGUAGE) + .size(MAX_PAGE_SIZE) + .minDocCount(1) + .order(Terms.Order.count(false)) + .subAggregation(sum(FIELD_DISTRIB_NCLOC).field(FIELD_DISTRIB_NCLOC)))); + + request.addAggregation(AggregationBuilders.nested(NCLOC_KEY, FIELD_MEASURES) + .subAggregation(AggregationBuilders.filter(NCLOC_KEY + "_filter", termQuery(FIELD_MEASURES_KEY, NCLOC_KEY)) + .subAggregation(sum(NCLOC_KEY + "_filter_sum").field(FIELD_MEASURES_VALUE)))); + + ProjectMeasuresStatistics.Builder statistics = ProjectMeasuresStatistics.builder(); + + SearchResponse response = request.get(); + statistics.setProjectCount(response.getHits().getTotalHits()); + Stream.of(NCLOC_KEY) + .map(metric -> (Nested) response.getAggregations().get(metric)) + .map(nested -> (Filter) nested.getAggregations().get(nested.getName() + "_filter")) + .map(filter -> (Sum) filter.getAggregations().get(filter.getName() + "_sum")) + .forEach(sum -> { + String metric = sum.getName().replace("_filter_sum", ""); + long value = Math.round(sum.getValue()); + statistics.setSum(metric, value); + }); + statistics.setProjectCountByLanguage(termsToMap(response.getAggregations().get(FIELD_LANGUAGES))); + Function bucketToNcloc = bucket -> Math.round(((Sum) bucket.getAggregations().get(FIELD_DISTRIB_NCLOC)).getValue()); + Map nclocByLanguage = Stream.of((Nested) response.getAggregations().get(FIELD_NCLOC_LANGUAGE_DISTRIBUTION)) + .map(nested -> (Terms) nested.getAggregations().get(nested.getName() + "_terms")) + .flatMap(terms -> terms.getBuckets().stream()) + .collect(MoreCollectors.uniqueIndex(Bucket::getKeyAsString, bucketToNcloc)); + statistics.setNclocByLanguage(nclocByLanguage); + + return statistics.build(); + } + + private static void addSort(ProjectMeasuresQuery query, SearchRequestBuilder requestBuilder) { + String sort = query.getSort(); + if (SORT_BY_NAME.equals(sort)) { + requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), query.isAsc() ? ASC : DESC); + } else if (SORT_BY_LAST_ANALYSIS_DATE.equals(sort)) { + requestBuilder.addSort(FIELD_ANALYSED_AT, query.isAsc() ? ASC : DESC); + } else if (ALERT_STATUS_KEY.equals(sort)) { + requestBuilder.addSort(FIELD_QUALITY_GATE_STATUS, query.isAsc() ? ASC : DESC); + requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC); + } else { + addMetricSort(query, requestBuilder, sort); + requestBuilder.addSort(DefaultIndexSettingsElement.SORTABLE_ANALYZER.subField(FIELD_NAME), ASC); + } + // last sort is by key in order to be deterministic when same value + requestBuilder.addSort(FIELD_KEY, ASC); + } + + private static void addMetricSort(ProjectMeasuresQuery query, SearchRequestBuilder requestBuilder, String sort) { + requestBuilder.addSort( + new FieldSortBuilder(FIELD_MEASURES_VALUE) + .setNestedPath(FIELD_MEASURES) + .setNestedFilter(termQuery(FIELD_MEASURES_KEY, sort)) + .order(query.isAsc() ? ASC : DESC)); + } + + private static void addRangeFacet(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder, Double... thresholds) { + esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, createRangeFacet(metricKey, thresholds))); + } + + private static void addRangeFacetIncludingNoData(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder, Double... thresholds) { + esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, + AggregationBuilders.filter("combined_" + metricKey, matchAllQuery()) + .subAggregation(createRangeFacet(metricKey, thresholds)) + .subAggregation(createNoDataFacet(metricKey)))); + } + + private static void addRatingFacet(SearchRequestBuilder esSearch, String metricKey, StickyFacetBuilder facetBuilder) { + esSearch.addAggregation(createStickyFacet(metricKey, facetBuilder, createRatingFacet(metricKey))); + } + + private static void addLanguagesFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder) { + esSearch.addAggregation(facetBuilder.buildStickyFacet(FIELD_LANGUAGES, FILTER_LANGUAGES, query.getLanguages().map(Set::toArray).orElseGet(() -> new Object[] {}))); + } + + private static void addTagsFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder) { + esSearch.addAggregation(facetBuilder.buildStickyFacet(FIELD_TAGS, FILTER_TAGS, query.getTags().map(Set::toArray).orElseGet(() -> new Object[] {}))); + } + + private static void addFacets(SearchRequestBuilder esSearch, SearchOptions options, Map filters, ProjectMeasuresQuery query) { + StickyFacetBuilder facetBuilder = new StickyFacetBuilder(matchAllQuery(), filters); + options.getFacets().stream() + .filter(FACET_FACTORIES::containsKey) + .map(FACET_FACTORIES::get) + .forEach(factory -> factory.addFacet(esSearch, query, facetBuilder)); + } + + private static AbstractAggregationBuilder createStickyFacet(String facetKey, StickyFacetBuilder facetBuilder, AbstractAggregationBuilder aggregationBuilder) { + BoolQueryBuilder facetFilter = facetBuilder.getStickyFacetFilter(facetKey); + return AggregationBuilders + .global(facetKey) + .subAggregation( + AggregationBuilders + .filter("facet_filter_" + facetKey, facetFilter) + .subAggregation(aggregationBuilder)); + } + + private static AbstractAggregationBuilder createRangeFacet(String metricKey, Double... thresholds) { + RangeAggregationBuilder rangeAgg = AggregationBuilders.range(metricKey) + .field(FIELD_MEASURES_VALUE); + final int lastIndex = thresholds.length - 1; + IntStream.range(0, thresholds.length) + .forEach(i -> { + if (i == 0) { + rangeAgg.addUnboundedTo(thresholds[0]); + rangeAgg.addRange(thresholds[0], thresholds[1]); + } else if (i == lastIndex) { + rangeAgg.addUnboundedFrom(thresholds[lastIndex]); + } else { + rangeAgg.addRange(thresholds[i], thresholds[i + 1]); + } + }); + + return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES) + .subAggregation( + AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_KEY, metricKey)) + .subAggregation(rangeAgg)); + } + + private static AbstractAggregationBuilder createNoDataFacet(String metricKey) { + return AggregationBuilders.filter( + "no_data_" + metricKey, + boolQuery().mustNot(nestedQuery(FIELD_MEASURES, termQuery(FIELD_MEASURES_KEY, metricKey), ScoreMode.Avg))); + } + + private static AbstractAggregationBuilder createRatingFacet(String metricKey) { + return AggregationBuilders.nested("nested_" + metricKey, FIELD_MEASURES) + .subAggregation( + AggregationBuilders.filter("filter_" + metricKey, termsQuery(FIELD_MEASURES_KEY, metricKey)) + .subAggregation(filters(metricKey, + new KeyedFilter("1", termQuery(FIELD_MEASURES_VALUE, 1d)), + new KeyedFilter("2", termQuery(FIELD_MEASURES_VALUE, 2d)), + new KeyedFilter("3", termQuery(FIELD_MEASURES_VALUE, 3d)), + new KeyedFilter("4", termQuery(FIELD_MEASURES_VALUE, 4d)), + new KeyedFilter("5", termQuery(FIELD_MEASURES_VALUE, 5d))))); + } + + private static AbstractAggregationBuilder createQualityGateFacet() { + return AggregationBuilders.filters( + ALERT_STATUS_KEY, + QUALITY_GATE_STATUS.entrySet().stream() + .map(entry -> new KeyedFilter(entry.getKey(), termQuery(FIELD_QUALITY_GATE_STATUS, entry.getValue()))) + .toArray(KeyedFilter[]::new)); + } + + private Map createFilters(ProjectMeasuresQuery query) { + Map filters = new HashMap<>(); + filters.put("__authorization", authorizationTypeSupport.createQueryFilter()); + Multimap metricCriterionMultimap = ArrayListMultimap.create(); + query.getMetricCriteria().forEach(metricCriterion -> metricCriterionMultimap.put(metricCriterion.getMetricKey(), metricCriterion)); + metricCriterionMultimap.asMap().forEach((key, value) -> { + BoolQueryBuilder metricFilters = boolQuery(); + value + .stream() + .map(ProjectMeasuresIndex::toQuery) + .forEach(metricFilters::must); + filters.put(key, metricFilters); + }); + + query.getQualityGateStatus() + .ifPresent(qualityGateStatus -> filters.put(ALERT_STATUS_KEY, termQuery(FIELD_QUALITY_GATE_STATUS, QUALITY_GATE_STATUS.get(qualityGateStatus.name())))); + + query.getProjectUuids() + .ifPresent(projectUuids -> filters.put("ids", termsQuery("_id", projectUuids))); + + query.getLanguages() + .ifPresent(languages -> filters.put(FILTER_LANGUAGES, termsQuery(FIELD_LANGUAGES, languages))); + + query.getOrganizationUuid() + .ifPresent(organizationUuid -> filters.put(FIELD_ORGANIZATION_UUID, termQuery(FIELD_ORGANIZATION_UUID, organizationUuid))); + + query.getTags() + .ifPresent(tags -> filters.put(FIELD_TAGS, termsQuery(FIELD_TAGS, tags))); + + query.getQueryText() + .map(ProjectsTextSearchQueryFactory::createQuery) + .ifPresent(queryBuilder -> filters.put("textQuery", queryBuilder)); + return filters; + } + + private static QueryBuilder toQuery(MetricCriterion criterion) { + if (criterion.isNoData()) { + return boolQuery().mustNot( + nestedQuery( + FIELD_MEASURES, + termQuery(FIELD_MEASURES_KEY, criterion.getMetricKey()), + ScoreMode.Avg)); + } + return nestedQuery( + FIELD_MEASURES, + boolQuery() + .filter(termQuery(FIELD_MEASURES_KEY, criterion.getMetricKey())) + .filter(toValueQuery(criterion)), + ScoreMode.Avg); + } + + private static QueryBuilder toValueQuery(MetricCriterion criterion) { + String fieldName = FIELD_MEASURES_VALUE; + + switch (criterion.getOperator()) { + case GT: + return rangeQuery(fieldName).gt(criterion.getValue()); + case GTE: + return rangeQuery(fieldName).gte(criterion.getValue()); + case LT: + return rangeQuery(fieldName).lt(criterion.getValue()); + case LTE: + return rangeQuery(fieldName).lte(criterion.getValue()); + case EQ: + return termQuery(fieldName, criterion.getValue()); + default: + throw new IllegalStateException("Metric criteria non supported: " + criterion.getOperator().name()); + } + } + + public List searchTags(@Nullable String textQuery, int size) { + int maxPageSize = 500; + checkArgument(size <= maxPageSize, "Page size must be lower than or equals to " + maxPageSize); + if (size <= 0) { + return emptyList(); + } + + TermsAggregationBuilder tagFacet = AggregationBuilders.terms(FIELD_TAGS) + .field(FIELD_TAGS) + .size(size) + .minDocCount(1) + .order(Terms.Order.term(true)); + if (textQuery != null) { + tagFacet.includeExclude(new IncludeExclude(".*" + escapeSpecialRegexChars(textQuery) + ".*", null)); + } + + SearchRequestBuilder searchQuery = client + .prepareSearch(INDEX_TYPE_PROJECT_MEASURES) + .setQuery(authorizationTypeSupport.createQueryFilter()) + .setFetchSource(false) + .setSize(0) + .addAggregation(tagFacet); + + Terms aggregation = searchQuery.get().getAggregations().get(FIELD_TAGS); + + return aggregation.getBuckets().stream() + .map(Bucket::getKeyAsString) + .collect(MoreCollectors.toList()); + } + + @FunctionalInterface + private interface FacetSetter { + void addFacet(SearchRequestBuilder esSearch, ProjectMeasuresQuery query, StickyFacetBuilder facetBuilder); + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java new file mode 100644 index 00000000000..17494bc8089 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectMeasuresQuery.java @@ -0,0 +1,194 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import javax.annotation.Nullable; +import org.sonar.api.measures.Metric; + +import static com.google.common.base.Preconditions.checkState; +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.Objects.requireNonNull; + +public class ProjectMeasuresQuery { + + public static final String SORT_BY_NAME = "name"; + public static final String SORT_BY_LAST_ANALYSIS_DATE = "analysisDate"; + + private List metricCriteria = new ArrayList<>(); + private Metric.Level qualityGateStatus; + private String organizationUuid; + private Set projectUuids; + private Set languages; + private Set tags; + private String sort = SORT_BY_NAME; + private boolean asc = true; + private String queryText; + + public ProjectMeasuresQuery addMetricCriterion(MetricCriterion metricCriterion) { + this.metricCriteria.add(metricCriterion); + return this; + } + + public List getMetricCriteria() { + return metricCriteria; + } + + public ProjectMeasuresQuery setQualityGateStatus(Metric.Level qualityGateStatus) { + this.qualityGateStatus = requireNonNull(qualityGateStatus); + return this; + } + + public Optional getQualityGateStatus() { + return Optional.ofNullable(qualityGateStatus); + } + + public ProjectMeasuresQuery setOrganizationUuid(@Nullable String organizationUuid) { + this.organizationUuid = organizationUuid; + return this; + } + + public Optional getOrganizationUuid() { + return Optional.ofNullable(organizationUuid); + } + + public ProjectMeasuresQuery setProjectUuids(@Nullable Set projectUuids) { + this.projectUuids = projectUuids; + return this; + } + + public Optional> getProjectUuids() { + return Optional.ofNullable(projectUuids); + } + + public ProjectMeasuresQuery setLanguages(@Nullable Set languages) { + this.languages = languages; + return this; + } + + public Optional> getLanguages() { + return Optional.ofNullable(languages); + } + + public ProjectMeasuresQuery setTags(@Nullable Set tags) { + this.tags = tags; + return this; + } + + public Optional> getTags() { + return Optional.ofNullable(tags); + } + + public Optional getQueryText() { + return Optional.ofNullable(queryText); + } + + public ProjectMeasuresQuery setQueryText(@Nullable String queryText) { + this.queryText = queryText; + return this; + } + + public String getSort() { + return sort; + } + + public ProjectMeasuresQuery setSort(String sort) { + this.sort = requireNonNull(sort, "Sort cannot be null"); + return this; + } + + public boolean isAsc() { + return asc; + } + + public ProjectMeasuresQuery setAsc(boolean asc) { + this.asc = asc; + return this; + } + + public static class MetricCriterion { + private final String metricKey; + private final Operator operator; + @Nullable + private final Double value; + + private MetricCriterion(String metricKey, @Nullable Operator operator, @Nullable Double value) { + this.metricKey = metricKey; + this.operator = operator; + this.value = value; + } + + public String getMetricKey() { + return metricKey; + } + + public Operator getOperator() { + checkDataAvailable(); + return operator; + } + + public double getValue() { + checkDataAvailable(); + return value; + } + + public boolean isNoData() { + return value == null; + } + + public static MetricCriterion createNoData(String metricKey) { + return new MetricCriterion(requireNonNull(metricKey), null, null); + } + + public static MetricCriterion create(String metricKey, Operator operator, double value) { + return new MetricCriterion(requireNonNull(metricKey), requireNonNull(operator), value); + } + + private void checkDataAvailable() { + checkState(!isNoData(), "The criterion for metric %s has no data", metricKey); + } + } + + public enum Operator { + LT("<"), LTE("<="), GT(">"), GTE(">="), EQ("="), IN("in"); + + String value; + + Operator(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Operator getByValue(String value) { + return stream(Operator.values()) + .filter(operator -> operator.getValue().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(format("Unknown operator '%s'", value))); + } + } + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java new file mode 100644 index 00000000000..bf6cc6c5234 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsEsModule.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import org.sonar.core.platform.Module; + +public class ProjectsEsModule extends Module { + @Override + protected void configureModule() { + add( + ProjectMeasuresIndexDefinition.class, + ProjectMeasuresIndex.class, + ProjectMeasuresIndexer.class); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java new file mode 100644 index 00000000000..71456795254 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/ProjectsTextSearchQueryFactory.java @@ -0,0 +1,129 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; +import org.apache.commons.lang.StringUtils; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.MatchQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.sonar.server.es.DefaultIndexSettings; + +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.matchQuery; +import static org.elasticsearch.index.query.QueryBuilders.prefixQuery; +import static org.sonar.server.es.DefaultIndexSettingsElement.SEARCH_GRAMS_ANALYZER; +import static org.sonar.server.es.DefaultIndexSettingsElement.SORTABLE_ANALYZER; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_KEY; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.FIELD_NAME; + +/** + * This class is used in order to do some advanced full text search on projects key and name + */ +class ProjectsTextSearchQueryFactory { + + private ProjectsTextSearchQueryFactory() { + // Only static methods + } + + static QueryBuilder createQuery(String queryText) { + BoolQueryBuilder featureQuery = boolQuery(); + Arrays.stream(ComponentTextSearchFeature.values()) + .map(f -> f.getQuery(queryText)) + .forEach(featureQuery::should); + return featureQuery; + } + + private enum ComponentTextSearchFeature { + + EXACT_IGNORE_CASE { + @Override + QueryBuilder getQuery(String queryText) { + return matchQuery(SORTABLE_ANALYZER.subField(FIELD_NAME), queryText) + .boost(2.5f); + } + }, + PREFIX { + @Override + QueryBuilder getQuery(String queryText) { + return prefixAndPartialQuery(queryText, FIELD_NAME, FIELD_NAME) + .boost(2f); + } + }, + PREFIX_IGNORE_CASE { + @Override + QueryBuilder getQuery(String queryText) { + String lowerCaseQueryText = queryText.toLowerCase(Locale.ENGLISH); + return prefixAndPartialQuery(lowerCaseQueryText, SORTABLE_ANALYZER.subField(FIELD_NAME), FIELD_NAME) + .boost(3f); + } + }, + PARTIAL { + @Override + QueryBuilder getQuery(String queryText) { + BoolQueryBuilder queryBuilder = boolQuery(); + split(queryText) + .map(text -> partialTermQuery(text, FIELD_NAME)) + .forEach(queryBuilder::must); + return queryBuilder + .boost(0.5f); + } + }, + KEY { + @Override + QueryBuilder getQuery(String queryText) { + return matchQuery(SORTABLE_ANALYZER.subField(FIELD_KEY), queryText) + .boost(50f); + } + }; + + abstract QueryBuilder getQuery(String queryText); + + protected Stream split(String queryText) { + return Arrays.stream( + queryText.split(DefaultIndexSettings.SEARCH_TERM_TOKENIZER_PATTERN)) + .filter(StringUtils::isNotEmpty); + } + + protected BoolQueryBuilder prefixAndPartialQuery(String queryText, String fieldName, String originalFieldName) { + BoolQueryBuilder queryBuilder = boolQuery(); + AtomicBoolean first = new AtomicBoolean(true); + split(queryText) + .map(queryTerm -> { + if (first.getAndSet(false)) { + return prefixQuery(fieldName, queryTerm); + } + return partialTermQuery(queryTerm, originalFieldName); + }) + .forEach(queryBuilder::must); + return queryBuilder; + } + + protected MatchQueryBuilder partialTermQuery(String queryTerm, String fieldName) { + // We will truncate the search to the maximum length of nGrams in the index. + // Otherwise the search would for sure not find any results. + String truncatedQuery = StringUtils.left(queryTerm, DefaultIndexSettings.MAXIMUM_NGRAM_LENGTH); + return matchQuery(SEARCH_GRAMS_ANALYZER.subField(fieldName), truncatedQuery); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/index/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/measure/index/package-info.java new file mode 100644 index 00000000000..ece66815ae9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/measure/index/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/measure/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/measure/package-info.java deleted file mode 100644 index 16ec2482de6..00000000000 --- a/server/sonar-server/src/main/java/org/sonar/server/measure/package-info.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2018 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.measure; - -import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java new file mode 100644 index 00000000000..72260764b4f --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexer.java @@ -0,0 +1,208 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import com.google.common.annotations.VisibleForTesting; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.elasticsearch.action.index.IndexRequest; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.es.EsQueueDto; +import org.sonar.server.es.BulkIndexer; +import org.sonar.server.es.BulkIndexer.Size; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.IndexingResult; +import org.sonar.server.es.OneToOneResilientIndexingListener; +import org.sonar.server.es.ProjectIndexer; + +import static java.util.Collections.emptyList; +import static org.sonar.core.util.stream.MoreCollectors.toArrayList; +import static org.sonar.core.util.stream.MoreCollectors.toSet; + +/** + * Populates the types "authorization" of each index requiring project + * authorization. + */ +public class PermissionIndexer implements ProjectIndexer { + + private final DbClient dbClient; + private final EsClient esClient; + private final Collection authorizationScopes; + private final Set indexTypes; + + public PermissionIndexer(DbClient dbClient, EsClient esClient, NeedAuthorizationIndexer... needAuthorizationIndexers) { + this(dbClient, esClient, Arrays.stream(needAuthorizationIndexers) + .map(NeedAuthorizationIndexer::getAuthorizationScope) + .collect(MoreCollectors.toList(needAuthorizationIndexers.length))); + } + + @VisibleForTesting + public PermissionIndexer(DbClient dbClient, EsClient esClient, Collection authorizationScopes) { + this.dbClient = dbClient; + this.esClient = esClient; + this.authorizationScopes = authorizationScopes; + this.indexTypes = authorizationScopes.stream() + .map(AuthorizationScope::getIndexType) + .collect(toSet(authorizationScopes.size())); + } + + @Override + public Set getIndexTypes() { + return indexTypes; + } + + @Override + public void indexOnStartup(Set uninitializedIndexTypes) { + // TODO do not load everything in memory. Db rows should be scrolled. + List authorizations = getAllAuthorizations(); + Stream scopes = getScopes(uninitializedIndexTypes); + index(authorizations, scopes, Size.LARGE); + } + + @VisibleForTesting + void index(List authorizations) { + index(authorizations, authorizationScopes.stream(), Size.REGULAR); + } + + @Override + public void indexOnAnalysis(String branchUuid) { + // nothing to do, permissions don't change during an analysis + } + + @Override + public Collection prepareForRecovery(DbSession dbSession, Collection projectUuids, ProjectIndexer.Cause cause) { + switch (cause) { + case MEASURE_CHANGE: + case PROJECT_KEY_UPDATE: + case PROJECT_TAGS_UPDATE: + // nothing to change. Measures, project key and tags are not part of this index + return emptyList(); + + case PROJECT_CREATION: + case PROJECT_DELETION: + case PERMISSION_CHANGE: + return insertIntoEsQueue(dbSession, projectUuids); + + default: + // defensive case + throw new IllegalStateException("Unsupported cause: " + cause); + } + } + + private Collection insertIntoEsQueue(DbSession dbSession, Collection projectUuids) { + List items = indexTypes.stream() + .flatMap(indexType -> projectUuids.stream().map(projectUuid -> EsQueueDto.create(indexType.format(), projectUuid, null, projectUuid))) + .collect(toArrayList()); + + dbClient.esQueueDao().insert(dbSession, items); + return items; + } + + private void index(Collection authorizations, Stream scopes, Size bulkSize) { + if (authorizations.isEmpty()) { + return; + } + + // index each authorization in each scope + scopes.forEach(scope -> { + IndexType indexType = scope.getIndexType(); + + BulkIndexer bulkIndexer = new BulkIndexer(esClient, indexType, bulkSize); + bulkIndexer.start(); + + authorizations.stream() + .filter(scope.getProjectPredicate()) + .map(dto -> newIndexRequest(dto, indexType)) + .forEach(bulkIndexer::add); + + bulkIndexer.stop(); + }); + } + + @Override + public IndexingResult index(DbSession dbSession, Collection items) { + IndexingResult result = new IndexingResult(); + + List bulkIndexers = items.stream() + .map(EsQueueDto::getDocType) + .distinct() + .map(IndexType::parse) + .filter(indexTypes::contains) + .map(indexType -> new BulkIndexer(esClient, indexType, Size.REGULAR, new OneToOneResilientIndexingListener(dbClient, dbSession, items))) + .collect(Collectors.toList()); + + if (bulkIndexers.isEmpty()) { + return result; + } + + bulkIndexers.forEach(BulkIndexer::start); + + PermissionIndexerDao permissionIndexerDao = new PermissionIndexerDao(); + Set remainingProjectUuids = items.stream().map(EsQueueDto::getDocId).collect(MoreCollectors.toHashSet()); + permissionIndexerDao.selectByUuids(dbClient, dbSession, remainingProjectUuids).forEach(p -> { + remainingProjectUuids.remove(p.getProjectUuid()); + bulkIndexers.forEach(bi -> bi.add(newIndexRequest(p, bi.getIndexType()))); + }); + + // the remaining references on projects that don't exist in db. They must + // be deleted from index. + remainingProjectUuids.forEach(projectUuid -> bulkIndexers.forEach(bi -> bi.addDeletion(bi.getIndexType(), projectUuid, projectUuid))); + + bulkIndexers.forEach(b -> result.add(b.stop())); + + return result; + } + + private static IndexRequest newIndexRequest(IndexPermissions dto, IndexType indexType) { + Map doc = new HashMap<>(); + if (dto.isAllowAnyone()) { + doc.put(IndexAuthorizationConstants.FIELD_ALLOW_ANYONE, true); + // no need to feed users and groups + } else { + doc.put(IndexAuthorizationConstants.FIELD_ALLOW_ANYONE, false); + doc.put(IndexAuthorizationConstants.FIELD_GROUP_IDS, dto.getGroupIds()); + doc.put(IndexAuthorizationConstants.FIELD_USER_IDS, dto.getUserIds()); + } + return new IndexRequest(indexType.getIndex(), indexType.getType()) + .id(dto.getProjectUuid()) + .routing(dto.getProjectUuid()) + .source(doc); + } + + private Stream getScopes(Set indexTypes) { + return authorizationScopes.stream() + .filter(scope -> indexTypes.contains(scope.getIndexType())); + } + + private List getAllAuthorizations() { + try (DbSession dbSession = dbClient.openSession(false)) { + return new PermissionIndexerDao().selectAll(dbClient, dbSession); + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java new file mode 100644 index 00000000000..22af562e7de --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/PermissionIndexerDao.java @@ -0,0 +1,210 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import com.google.common.collect.ImmutableList; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang.StringUtils; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; + +import static org.apache.commons.lang.StringUtils.repeat; +import static org.sonar.db.DatabaseUtils.executeLargeInputs; + +/** + * No streaming because of union of joins -> no need to use ResultSetIterator + */ +public class PermissionIndexerDao { + + private enum RowKind { + USER, GROUP, ANYONE, NONE + } + + private static final String SQL_TEMPLATE = "SELECT " + + " project_authorization.kind as kind, " + + " project_authorization.project as project, " + + " project_authorization.user_id as user_id, " + + " project_authorization.group_id as group_id, " + + " project_authorization.qualifier as qualifier " + + "FROM ( " + + + // users + + " SELECT '" + RowKind.USER + "' as kind," + + " projects.uuid AS project, " + + " projects.qualifier AS qualifier, " + + " user_roles.user_id AS user_id, " + + " NULL AS group_id " + + " FROM projects " + + " INNER JOIN user_roles ON user_roles.resource_id = projects.id AND user_roles.role = 'user' " + + " WHERE " + + " (projects.qualifier = 'TRK' " + + " or projects.qualifier = 'VW' " + + " or projects.qualifier = 'APP') " + + " AND projects.copy_component_uuid is NULL " + + " {projectsCondition} " + + " UNION " + + + // groups + + " SELECT '" + RowKind.GROUP + "' as kind," + + " projects.uuid AS project, " + + " projects.qualifier AS qualifier, " + + " NULL AS user_id, " + + " groups.id AS group_id " + + " FROM projects " + + " INNER JOIN group_roles ON group_roles.resource_id = projects.id AND group_roles.role = 'user' " + + " INNER JOIN groups ON groups.id = group_roles.group_id " + + " WHERE " + + " (projects.qualifier = 'TRK' " + + " or projects.qualifier = 'VW' " + + " or projects.qualifier = 'APP') " + + " AND projects.copy_component_uuid is NULL " + + " {projectsCondition} " + + " AND group_id IS NOT NULL " + + " UNION " + + + // public projects are accessible to any one + + " SELECT '" + RowKind.ANYONE + "' as kind," + + " projects.uuid AS project, " + + " projects.qualifier AS qualifier, " + + " NULL AS user_id, " + + " NULL AS group_id " + + " FROM projects " + + " WHERE " + + " (projects.qualifier = 'TRK' " + + " or projects.qualifier = 'VW' " + + " or projects.qualifier = 'APP') " + + " AND projects.copy_component_uuid is NULL " + + " AND projects.private = ? " + + " {projectsCondition} " + + " UNION " + + + // private project is returned when no authorization + " SELECT '" + RowKind.NONE + "' as kind," + + " projects.uuid AS project, " + + " projects.qualifier AS qualifier, " + + " NULL AS user_id, " + + " NULL AS group_id " + + " FROM projects " + + " WHERE " + + " (projects.qualifier = 'TRK' " + + " or projects.qualifier = 'VW' " + + " or projects.qualifier = 'APP') " + + " AND projects.copy_component_uuid is NULL " + + " AND projects.private = ? " + + " {projectsCondition} " + + + " ) project_authorization"; + + List selectAll(DbClient dbClient, DbSession session) { + return doSelectByProjects(dbClient, session, Collections.emptyList()); + } + + List selectByUuids(DbClient dbClient, DbSession session, Collection projectOrViewUuids) { + return executeLargeInputs(projectOrViewUuids, subProjectOrViewUuids -> doSelectByProjects(dbClient, session, subProjectOrViewUuids)); + } + + private static List doSelectByProjects(DbClient dbClient, DbSession session, List projectUuids) { + try { + Map dtosByProjectUuid = new HashMap<>(); + try (PreparedStatement stmt = createStatement(dbClient, session, projectUuids); + ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + processRow(rs, dtosByProjectUuid); + } + return ImmutableList.copyOf(dtosByProjectUuid.values()); + } + } catch (SQLException e) { + throw new IllegalStateException("Fail to select authorizations", e); + } + } + + private static PreparedStatement createStatement(DbClient dbClient, DbSession session, List projectUuids) throws SQLException { + String sql; + if (projectUuids.isEmpty()) { + sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", ""); + } else { + sql = StringUtils.replace(SQL_TEMPLATE, "{projectsCondition}", " AND projects.uuid in (" + repeat("?", ", ", projectUuids.size()) + ")"); + } + PreparedStatement stmt = dbClient.getMyBatis().newScrollingSelectStatement(session, sql); + int index = 1; + // query for RowKind.USER + index = populateProjectUuidPlaceholders(stmt, projectUuids, index); + // query for RowKind.GROUP + index = populateProjectUuidPlaceholders(stmt, projectUuids, index); + // query for RowKind.ANYONE + index = setPrivateProjectPlaceHolder(stmt, index, false); + index = populateProjectUuidPlaceholders(stmt, projectUuids, index); + // query for RowKind.NONE + index = setPrivateProjectPlaceHolder(stmt, index, true); + populateProjectUuidPlaceholders(stmt, projectUuids, index); + return stmt; + } + + private static int populateProjectUuidPlaceholders(PreparedStatement stmt, List projectUuids, int index) throws SQLException { + int newIndex = index; + for (String projectUuid : projectUuids) { + stmt.setString(newIndex, projectUuid); + newIndex++; + } + return newIndex; + } + + private static int setPrivateProjectPlaceHolder(PreparedStatement stmt, int index, boolean isPrivate) throws SQLException { + int newIndex = index; + stmt.setBoolean(newIndex, isPrivate); + newIndex++; + return newIndex; + } + + private static void processRow(ResultSet rs, Map dtosByProjectUuid) throws SQLException { + RowKind rowKind = RowKind.valueOf(rs.getString(1)); + String projectUuid = rs.getString(2); + + IndexPermissions dto = dtosByProjectUuid.get(projectUuid); + if (dto == null) { + String qualifier = rs.getString(5); + dto = new IndexPermissions(projectUuid, qualifier); + dtosByProjectUuid.put(projectUuid, dto); + } + switch (rowKind) { + case NONE: + break; + case USER: + dto.addUserId(rs.getInt(3)); + break; + case GROUP: + dto.addGroupId(rs.getInt(4)); + break; + case ANYONE: + dto.allowAnyone(); + break; + } + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/WebAuthorizationTypeSupport.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/WebAuthorizationTypeSupport.java new file mode 100644 index 00000000000..3a330a9d629 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/WebAuthorizationTypeSupport.java @@ -0,0 +1,78 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.Optional; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.join.query.JoinQueryBuilders; +import org.sonar.api.server.ServerSide; +import org.sonar.db.user.GroupDto; +import org.sonar.server.user.UserSession; + +import static org.elasticsearch.index.query.QueryBuilders.boolQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_ALLOW_ANYONE; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_GROUP_IDS; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.FIELD_USER_IDS; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; + +@ServerSide +public class WebAuthorizationTypeSupport { + + private final UserSession userSession; + + public WebAuthorizationTypeSupport(UserSession userSession) { + this.userSession = userSession; + } + + /** + * Build a filter to restrict query to the documents on which + * user has read access. + */ + public QueryBuilder createQueryFilter() { + if (userSession.isRoot()) { + return QueryBuilders.matchAllQuery(); + } + + Integer userId = userSession.getUserId(); + BoolQueryBuilder filter = boolQuery(); + + // anyone + filter.should(QueryBuilders.termQuery(FIELD_ALLOW_ANYONE, true)); + + // users + Optional.ofNullable(userId) + .map(Integer::longValue) + .ifPresent(id -> filter.should(termQuery(FIELD_USER_IDS, id))); + + // groups + userSession.getGroups() + .stream() + .map(GroupDto::getId) + .forEach(groupId -> filter.should(termQuery(FIELD_GROUP_IDS, groupId))); + + return JoinQueryBuilders.hasParentQuery( + TYPE_AUTHORIZATION, + QueryBuilders.boolQuery().filter(filter), + false); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/permission/index/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/permission/index/package-info.java new file mode 100644 index 00000000000..b09dbe2ed70 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/permission/index/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java index 1b2276258e5..1a33812cd88 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java @@ -48,6 +48,7 @@ import org.sonar.server.app.WebServerProcessLogging; import org.sonar.server.config.ConfigurationProvider; import org.sonar.server.es.EsModule; import org.sonar.server.issue.index.IssueIndex; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.platform.LogServerVersion; import org.sonar.server.platform.Platform; import org.sonar.server.platform.ServerFileSystemImpl; @@ -117,6 +118,7 @@ public class PlatformLevel1 extends PlatformLevel { DaoModule.class, // Elasticsearch + WebAuthorizationTypeSupport.class, EsModule.class, // rules/qprofiles diff --git a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparison.java b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparison.java index 9f336ebc6a3..7dd42b598ba 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparison.java +++ b/server/sonar-server/src/main/java/org/sonar/server/qualityprofile/QProfileComparison.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; -import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.rule.RuleKey; import org.sonar.api.server.ServerSide; import org.sonar.db.DbClient; @@ -40,7 +39,6 @@ import org.sonar.db.qualityprofile.OrgActiveRuleDto; import org.sonar.db.qualityprofile.QProfileDto; @ServerSide -@ComputeEngineSide public class QProfileComparison { private final DbClient dbClient; diff --git a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java index 280e9100227..4875c20dbca 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/securityreport/ws/ShowAction.java @@ -38,7 +38,7 @@ import static java.lang.Integer.parseInt; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; import static org.sonar.api.web.UserRole.USER; -import static org.sonar.server.issue.IssueQuery.UNKNOWN_STANDARD; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; import static org.sonar.server.ws.WsUtils.writeProtobuf; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_OWASP_TOP_10; import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SANS_TOP_25; diff --git a/server/sonar-server/src/main/java/org/sonar/server/telemetry/TelemetryDataLoader.java b/server/sonar-server/src/main/java/org/sonar/server/telemetry/TelemetryDataLoader.java index 635499c7ee9..3fb5eec7733 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/telemetry/TelemetryDataLoader.java +++ b/server/sonar-server/src/main/java/org/sonar/server/telemetry/TelemetryDataLoader.java @@ -23,7 +23,6 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.Map; import java.util.function.Function; -import org.sonar.api.ce.ComputeEngineSide; import org.sonar.api.platform.Server; import org.sonar.api.server.ServerSide; import org.sonar.core.platform.PlatformEditionProvider; @@ -39,7 +38,6 @@ import org.sonar.server.telemetry.TelemetryData.Database; import org.sonar.server.user.index.UserIndex; import org.sonar.server.user.index.UserQuery; -@ComputeEngineSide @ServerSide public class TelemetryDataLoader { private final Server server; diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/BaseUserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/BaseUserSession.java new file mode 100644 index 00000000000..51c591cd62f --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/BaseUserSession.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.user; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.sonar.core.permission.ProjectPermissions; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.OrganizationPermission; + +import static org.apache.commons.lang.StringUtils.defaultString; + +public abstract class BaseUserSession implements UserSession { + @Override + public final boolean hasPermission(OrganizationPermission permission, OrganizationDto organization) { + return hasPermission(permission, organization.getUuid()); + } + + @Override + public final boolean hasPermission(OrganizationPermission permission, String organizationUuid) { + return isRoot() || hasPermissionImpl(permission, organizationUuid); + } + + protected abstract boolean hasPermissionImpl(OrganizationPermission permission, String organizationUuid); + + @Override + public final boolean hasComponentPermission(String permission, ComponentDto component) { + if (isRoot()) { + return true; + } + String projectUuid = defaultString(component.getMainBranchProjectUuid(), component.projectUuid()); + return hasProjectUuidPermission(permission, projectUuid); + } + + @Override + public final boolean hasComponentUuidPermission(String permission, String componentUuid) { + if (isRoot()) { + return true; + } + Optional projectUuid = componentUuidToProjectUuid(componentUuid); + return projectUuid + .map(s -> hasProjectUuidPermission(permission, s)) + .orElse(false); + } + + protected abstract Optional componentUuidToProjectUuid(String componentUuid); + + protected abstract boolean hasProjectUuidPermission(String permission, String projectUuid); + + @Override + public final boolean hasMembership(OrganizationDto organization) { + return isRoot() || hasMembershipImpl(organization); + } + + protected abstract boolean hasMembershipImpl(OrganizationDto organization); + + @Override + public final List keepAuthorizedComponents(String permission, Collection components) { + if (isRoot()) { + return new ArrayList<>(components); + } + return doKeepAuthorizedComponents(permission, components); + } + + /** + * Naive implementation, to be overridden if needed + */ + protected List doKeepAuthorizedComponents(String permission, Collection components) { + boolean allowPublicComponent = ProjectPermissions.PUBLIC_PERMISSIONS.contains(permission); + return components.stream() + .filter(c -> (allowPublicComponent && !c.isPrivate()) || hasComponentPermission(permission, c)) + .collect(MoreCollectors.toList()); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java b/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java new file mode 100644 index 00000000000..645254afcf3 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java @@ -0,0 +1,192 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.user; + +import java.util.Collection; +import java.util.List; +import javax.annotation.CheckForNull; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.OrganizationPermission; +import org.sonar.db.user.GroupDto; + +public interface UserSession { + + /** + * Login of the authenticated user. Returns {@code null} + * if {@link #isLoggedIn()} is {@code false}. + */ + @CheckForNull + String getLogin(); + + /** + * Uuid of the authenticated user. Returns {@code null} + * if {@link #isLoggedIn()} is {@code false}. + */ + @CheckForNull + String getUuid(); + + /** + * Name of the authenticated user. Returns {@code null} + * if {@link #isLoggedIn()} is {@code false}. + */ + @CheckForNull + String getName(); + + /** + * Database ID of the authenticated user. Returns {@code null} + * if {@link #isLoggedIn()} is {@code false}. + */ + @CheckForNull + Integer getUserId(); + + /** + * The groups that the logged-in user is member of. An empty + * collection is returned if {@link #isLoggedIn()} is {@code false}. + */ + Collection getGroups(); + + /** + * Whether the user is logged-in or anonymous. + */ + boolean isLoggedIn(); + + /** + * Whether the user has root privileges. If {@code true}, then user automatically + * benefits from all the permissions on all organizations and projects. + */ + boolean isRoot(); + + /** + * Ensures that {@link #isRoot()} returns {@code true} otherwise throws a + * {@link org.sonar.server.exceptions.ForbiddenException}. + */ + UserSession checkIsRoot(); + + /** + * Ensures that user is logged in otherwise throws {@link org.sonar.server.exceptions.UnauthorizedException}. + */ + UserSession checkLoggedIn(); + + /** + * Returns {@code true} if the permission is granted on the organization, otherwise {@code false}. + * + * If the organization does not exist, then returns {@code false}. + * + * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if + * organization does not exist. + */ + boolean hasPermission(OrganizationPermission permission, OrganizationDto organization); + + boolean hasPermission(OrganizationPermission permission, String organizationUuid); + + /** + * Ensures that {@link #hasPermission(OrganizationPermission, OrganizationDto)} is {@code true}, + * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. + */ + UserSession checkPermission(OrganizationPermission permission, OrganizationDto organization); + + UserSession checkPermission(OrganizationPermission permission, String organizationUuid); + + /** + * Returns {@code true} if the permission is granted to user on the component, + * otherwise {@code false}. + * + * If the component does not exist, then returns {@code false}. + * + * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if + * component does not exist. + * + * If the permission is not granted, then the organization permission is _not_ checked. + * + * @param component non-null component. + * @param permission project permission as defined by {@link org.sonar.core.permission.ProjectPermissions} + */ + boolean hasComponentPermission(String permission, ComponentDto component); + + /** + * Using {@link #hasComponentPermission(String, ComponentDto)} is recommended + * because it does not have to load project if the referenced component + * is not a project. + * + * @deprecated use {@link #hasComponentPermission(String, ComponentDto)} instead + */ + @Deprecated + boolean hasComponentUuidPermission(String permission, String componentUuid); + + /** + * Return the subset of specified components which the user has granted permission. + * An empty list is returned if input is empty or if no components are allowed to be + * accessed. + * If the input is ordered, then the returned components are in the same order. + * The duplicated components are returned duplicated too. + */ + List keepAuthorizedComponents(String permission, Collection components); + + /** + * Ensures that {@link #hasComponentPermission(String, ComponentDto)} is {@code true}, + * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. + */ + UserSession checkComponentPermission(String projectPermission, ComponentDto component); + + /** + * Ensures that {@link #hasComponentUuidPermission(String, String)} is {@code true}, + * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. + * + * @deprecated use {@link #checkComponentPermission(String, ComponentDto)} instead + */ + @Deprecated + UserSession checkComponentUuidPermission(String permission, String componentUuid); + + /** + * Whether user can administrate system, for example for using cross-organizations services + * like update center, system info or management of users. + * + * Returns {@code true} if: + *
    + *
  • {@link #isRoot()} is {@code true}
  • + *
  • organization feature is disabled and user is administrator of the (single) default organization
  • + *
+ */ + boolean isSystemAdministrator(); + + /** + * Ensures that {@link #isSystemAdministrator()} is {@code true}, + * otherwise throws {@link org.sonar.server.exceptions.ForbiddenException}. + */ + UserSession checkIsSystemAdministrator(); + + /** + * Returns {@code true} if the user is member of the organization, otherwise {@code false}. + * + * If the organization does not exist, then returns {@code false}. + * + * Always returns {@code true} if {@link #isRoot()} is {@code true}, even if + * organization does not exist. + */ + boolean hasMembership(OrganizationDto organization); + + /** + * Ensures that {@link #hasMembership(OrganizationDto)} is {@code true}, + * otherwise throws a {@link org.sonar.server.exceptions.ForbiddenException}. + */ + UserSession checkMembership(OrganizationDto organization); + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java index 25aefe358fc..25d2685a9b1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/branch/pr/ws/ListActionTest.java @@ -45,8 +45,8 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; import org.sonarqube.ws.MediaTypes; @@ -90,7 +90,7 @@ public class ListActionTest { private ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); private PermissionIndexerTester permissionIndexerTester = new PermissionIndexerTester(es, issueIndexer); public WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(), userSession, new ComponentFinder(db.getDbClient(), resourceTypes), issueIndex)); diff --git a/server/sonar-server/src/test/java/org/sonar/server/branch/ws/ListActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/branch/ws/ListActionTest.java index a865ac049cc..94f379c6700 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/branch/ws/ListActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/branch/ws/ListActionTest.java @@ -40,8 +40,8 @@ import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; import org.sonarqube.ws.Common.BranchType; @@ -83,7 +83,7 @@ public class ListActionTest { private ResourceTypes resourceTypes = new ResourceTypesRule().setRootQualifiers(PROJECT); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); private PermissionIndexerTester permissionIndexerTester = new PermissionIndexerTester(es, issueIndexer); private MetricDto qualityGateStatus; diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java new file mode 100644 index 00000000000..e96e0e366aa --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexCombinationTest.java @@ -0,0 +1,68 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.stream.IntStream; +import org.junit.Test; +import org.sonar.api.resources.Qualifiers; +import org.sonar.db.component.ComponentDto; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentIndexCombinationTest extends ComponentIndexTest { + + @Test + public void return_empty_list_if_no_fields_match_query() { + indexProject("struts", "Apache Struts"); + + assertThat(index.searchSuggestions(SuggestionQuery.builder().setQuery("missing").build()).isEmpty()).isTrue(); + } + + @Test + public void should_not_return_components_that_do_not_match_at_all() { + indexProject("banana", "Banana Project 1"); + + assertNoSearchResults("Apple"); + } + + @Test + public void filter_results_by_qualifier() { + ComponentDto project = indexProject("struts", "Apache Struts"); + indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); + + assertSearchResults(SuggestionQuery.builder().setQuery("struts").setQualifiers(singletonList(Qualifiers.PROJECT)).build(), project); + } + + @Test + public void should_limit_the_number_of_results() { + IntStream.rangeClosed(0, 10).forEach(i -> indexProject("sonarqube" + i, "SonarQube" + i)); + + assertSearch(SuggestionQuery.builder().setQuery("sonarqube").setLimit(5).setQualifiers(singletonList(Qualifiers.PROJECT)).build()).hasSize(5); + } + + @Test + public void should_not_support_wildcards() { + indexProject("theKey", "the name"); + + assertNoSearchResults("*t*"); + assertNoSearchResults("th?Key"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java new file mode 100644 index 00000000000..7f781bff032 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureExactTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public class ComponentIndexFeatureExactTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(query -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.EXACT_IGNORE_CASE); + } + + @Test + public void scoring_cares_about_exact_matches() { + ComponentDto project1 = indexProject("project1", "LongNameLongNameLongNameLongNameSonarQube"); + ComponentDto project2 = indexProject("project2", "LongNameLongNameLongNameLongNameSonarQubeX"); + + SuggestionQuery query1 = SuggestionQuery.builder() + .setQuery("LongNameLongNameLongNameLongNameSonarQube") + .setQualifiers(Collections.singletonList(PROJECT)) + .build(); + assertSearch(query1).containsExactly(uuids(project1, project2)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java new file mode 100644 index 00000000000..7028889347f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureFavoriteTest.java @@ -0,0 +1,73 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +import static com.google.common.collect.ImmutableSet.of; +import static java.util.Collections.singletonList; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.index.query.QueryBuilders.termQuery; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.server.component.index.ComponentIndexDefinition.FIELD_KEY; + +public class ComponentIndexFeatureFavoriteTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(q -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.FAVORITE); + } + + @Test + public void scoring_cares_about_favorites() { + ComponentDto project1 = indexProject("sonarqube", "SonarQube"); + ComponentDto project2 = indexProject("recent", "SonarQube Recently"); + + SuggestionQuery query1 = SuggestionQuery.builder() + .setQuery("SonarQube") + .setQualifiers(singletonList(PROJECT)) + .setFavoriteKeys(of(project1.getDbKey())) + .build(); + assertSearch(query1).containsExactly(uuids(project1, project2)); + + SuggestionQuery query2 = SuggestionQuery.builder() + .setQuery("SonarQube") + .setQualifiers(singletonList(PROJECT)) + .setFavoriteKeys(of(project2.getDbKey())) + .build(); + assertSearch(query2).containsExactly(uuids(project2, project1)); + } + + @Test + public void irrelevant_favorites_are_not_returned() { + features.set(q -> termQuery(FIELD_KEY, "non-existing-value"), ComponentTextSearchFeatureRepertoire.FAVORITE); + ComponentDto project1 = indexProject("foo", "foo"); + + SuggestionQuery query1 = SuggestionQuery.builder() + .setQuery("bar") + .setQualifiers(singletonList(PROJECT)) + .setFavoriteKeys(of(project1.getDbKey())) + .build(); + assertSearch(query1).isEmpty(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java new file mode 100644 index 00000000000..79c1ba832c4 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureKeyTest.java @@ -0,0 +1,84 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +public class ComponentIndexFeatureKeyTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(ComponentTextSearchFeatureRepertoire.KEY); + } + + @Test + public void should_search_projects_by_exact_case_insensitive_key() { + ComponentDto project1 = indexProject("keyOne", "Project One"); + indexProject("keyTwo", "Project Two"); + + assertSearchResults("keyOne", project1); + assertSearchResults("keyone", project1); + assertSearchResults("KEYone", project1); + } + + @Test + public void should_search_project_with_dot_in_key() { + ComponentDto project = indexProject("org.sonarqube", "SonarQube"); + + assertSearchResults("org.sonarqube", project); + assertNoSearchResults("orgsonarqube"); + } + + @Test + public void should_search_project_with_dash_in_key() { + ComponentDto project = indexProject("org-sonarqube", "SonarQube"); + + assertSearchResults("org-sonarqube", project); + assertNoSearchResults("orgsonarqube"); + } + + @Test + public void should_search_project_with_colon_in_key() { + ComponentDto project = indexProject("org:sonarqube", "Quality Product"); + + assertSearchResults("org:sonarqube", project); + assertNoSearchResults("orgsonarqube"); + assertNoSearchResults("org-sonarqube"); + assertNoSearchResults("org_sonarqube"); + } + + @Test + public void should_search_project_with_all_special_characters_in_key() { + ComponentDto project = indexProject("org.sonarqube:sonar-sérvèr_ç", "SonarQube"); + + assertSearchResults("org.sonarqube:sonar-sérvèr_ç", project); + } + + @Test + public void should_not_return_results_when_searching_by_partial_key() { + indexProject("theKey", "some name"); + + assertNoSearchResults("theke"); + assertNoSearchResults("hekey"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java new file mode 100644 index 00000000000..ee014835fc0 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePartialTest.java @@ -0,0 +1,123 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +public class ComponentIndexFeaturePartialTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); + } + + @Test + public void search_projects_by_exact_name() { + ComponentDto struts = indexProject("struts", "Apache Struts"); + indexProject("sonarqube", "SonarQube"); + + assertSearchResults("Apache Struts", struts); + assertSearchResults("APACHE STRUTS", struts); + assertSearchResults("APACHE struTS", struts); + } + + @Test + public void search_file_with_long_name() { + ComponentDto project = indexProject("struts", "Apache Struts"); + ComponentDto file1 = indexFile(project, "src/main/java/DefaultRubyComponentServiceTestManagerFactory.java", "DefaultRubyComponentServiceTestManagerFactory.java"); + + assertSearchResults("DefaultRubyComponentServiceTestManagerFactory", file1); + assertSearchResults("DefaultRubyComponentServiceTestManagerFactory.java", file1); + assertSearchResults("RubyComponentServiceTestManager", file1); + assertSearchResults("te", file1); + } + + @Test + public void should_search_by_name_with_two_characters() { + ComponentDto project = indexProject("struts", "Apache Struts"); + + assertSearchResults("st", project); + assertSearchResults("tr", project); + } + + @Test + public void search_projects_by_partial_name() { + ComponentDto struts = indexProject("struts", "Apache Struts"); + + assertSearchResults("truts", struts); + assertSearchResults("pache", struts); + assertSearchResults("apach", struts); + assertSearchResults("che stru", struts); + } + + @Test + public void search_projects_and_files_by_partial_name() { + ComponentDto project = indexProject("struts", "Apache Struts"); + ComponentDto file1 = indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); + indexFile(project, "src/main/java/Foo.java", "Foo.java"); + + assertSearchResults("struts", project, file1); + assertSearchResults("Struts", project, file1); + assertSearchResults("StrutsManager", file1); + assertSearchResults("STRUTSMAN", file1); + assertSearchResults("utsManag", file1); + } + + @Test + public void should_find_file_by_file_extension() { + ComponentDto project = indexProject("struts", "Apache Struts"); + ComponentDto file1 = indexFile(project, "src/main/java/StrutsManager.java", "StrutsManager.java"); + ComponentDto file2 = indexFile(project, "src/main/java/Foo.java", "Foo.java"); + + assertSearchResults(".java", file1, file2); + assertSearchResults("manager.java", file1); + + // do not match + assertNoSearchResults("somethingStrutsManager.java"); + } + + @Test + public void should_search_for_word_and_suffix() { + assertFileMatches("plugin java", "AbstractPluginFactory.java"); + } + + @Test + public void should_search_for_word_and_suffix_in_any_order() { + assertFileMatches("java plugin", "AbstractPluginFactory.java"); + } + + @Test + public void should_search_for_two_words() { + assertFileMatches("abstract factory", "AbstractPluginFactory.java"); + } + + @Test + public void should_search_for_two_words_in_any_order() { + assertFileMatches("factory abstract", "AbstractPluginFactory.java"); + } + + @Test + public void should_require_at_least_one_matching_word() { + assertNoFileMatches("monitor object", "AbstractPluginFactory.java"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java new file mode 100644 index 00000000000..e24735268f8 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeaturePrefixTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Before; +import org.junit.Test; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +public class ComponentIndexFeaturePrefixTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(ComponentTextSearchFeatureRepertoire.PREFIX, ComponentTextSearchFeatureRepertoire.PREFIX_IGNORE_CASE); + } + + @Test + public void should_find_prefix() { + assertResultOrder("comp", "component"); + } + + @Test + public void should_find_exact_match() { + assertResultOrder("component.js", "component.js"); + } + + @Test + public void should_not_find_partially() { + assertNoFileMatches("component.js", "my_component.js"); + } + + @Test + public void should_be_able_to_ignore_case() { + features.set(ComponentTextSearchFeatureRepertoire.PREFIX_IGNORE_CASE); + assertResultOrder("cOmPoNeNt.Js", "CoMpOnEnT.jS"); + } + + @Test + public void should_prefer_matching_case() { + assertResultOrder("cOmPoNeNt.Js", "cOmPoNeNt.Js", "CoMpOnEnT.jS"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java new file mode 100644 index 00000000000..54fa6595b83 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexFeatureRecentlyBrowsedTest.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +import static com.google.common.collect.ImmutableSet.of; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public class ComponentIndexFeatureRecentlyBrowsedTest extends ComponentIndexTest { + + @Before + public void before() { + features.set(query -> matchAllQuery(), ComponentTextSearchFeatureRepertoire.RECENTLY_BROWSED); + } + + @Test + public void scoring_cares_about_recently_browsed() { + ComponentDto project1 = indexProject("sonarqube", "SonarQube"); + ComponentDto project2 = indexProject("recent", "SonarQube Recently"); + + SuggestionQuery query1 = SuggestionQuery.builder() + .setQuery("SonarQube") + .setQualifiers(Collections.singletonList(PROJECT)) + .setRecentlyBrowsedKeys(of(project1.getDbKey())) + .build(); + assertSearch(query1).containsExactly(uuids(project1, project2)); + + SuggestionQuery query2 = SuggestionQuery.builder() + .setQuery("SonarQube") + .setQualifiers(Collections.singletonList(PROJECT)) + .setRecentlyBrowsedKeys(of(project2.getDbKey())) + .build(); + assertSearch(query2).containsExactly(uuids(project2, project1)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java new file mode 100644 index 00000000000..d0ff9409dbd --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexHighlightTest.java @@ -0,0 +1,76 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Collections; +import java.util.Optional; +import java.util.stream.Stream; +import org.junit.Test; +import org.sonar.api.resources.Qualifiers; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentIndexHighlightTest extends ComponentIndexTest { + + @Test + public void should_escape_html() { + assertHighlighting("quick< brown fox", "brown", "quick< brown fox"); + } + + @Test + public void should_highlight_partial_name() { + assertHighlighting("quickbrownfox", "brown", "quickbrownfox"); + } + + @Test + public void should_highlight_prefix() { + assertHighlighting("quickbrownfox", "quick", "quickbrownfox"); + } + + @Test + public void should_highlight_suffix() { + assertHighlighting("quickbrownfox", "fox", "quickbrownfox"); + } + + @Test + public void should_highlight_multiple_words() { + assertHighlighting("quickbrownfox", "fox bro", "quickbrownfox"); + } + + @Test + public void should_highlight_multiple_connected_words() { + assertHighlighting("quickbrownfox", "fox brown", "quickbrownfox"); + } + + private void assertHighlighting(String fileName, String search, String expectedHighlighting) { + indexFile(fileName); + + SuggestionQuery query = SuggestionQuery.builder() + .setQuery(search) + .setQualifiers(Collections.singletonList(Qualifiers.FILE)) + .build(); + Stream results = index.searchSuggestions(query, features.get()).getQualifiers(); + + assertThat(results).flatExtracting(ComponentHitsPerQualifier::getHits) + .extracting(ComponentHit::getHighlightedText) + .extracting(Optional::get) + .containsExactly(expectedHighlighting); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java new file mode 100644 index 00000000000..24597b6dac4 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexLoginTest.java @@ -0,0 +1,81 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; + +import static org.sonar.db.user.GroupTesting.newGroupDto; +import static org.sonar.db.user.UserTesting.newUserDto; + +public class ComponentIndexLoginTest extends ComponentIndexTest { + + @Test + public void should_filter_unauthorized_results() { + indexer.index(newProject("sonarqube", "Quality Product")); + + // do not give any permissions to that project + + assertNoSearchResults("sonarqube"); + assertNoSearchResults("Quality Product"); + } + + @Test + public void should_find_project_for_which_the_user_has_direct_permission() { + UserDto user = newUserDto(); + userSession.logIn(user); + + ComponentDto project = newProject("sonarqube", "Quality Product"); + indexer.index(project); + + assertNoSearchResults("sonarqube"); + + // give the user explicit access + authorizationIndexerTester.allowOnlyUser(project, user); + assertSearchResults("sonarqube", project); + } + + @Test + public void should_find_project_for_which_the_user_has_indirect_permission_through_group() { + GroupDto group = newGroupDto(); + userSession.logIn().setGroups(group); + + ComponentDto project = newProject("sonarqube", "Quality Product"); + indexer.index(project); + + assertNoSearchResults("sonarqube"); + + // give the user implicit access (though group) + authorizationIndexerTester.allowOnlyGroup(project, group); + assertSearchResults("sonarqube", project); + } + + @Test + public void do_not_check_permissions_when_logged_in_user_is_root() { + userSession.logIn().setRoot(); + ComponentDto project = newProject("sonarqube", "Quality Product"); + indexer.index(project); + // do not give any permissions to that project + + assertSearchResults("sonarqube", project); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java new file mode 100644 index 00000000000..6eda6e347c1 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexMultipleWordsTest.java @@ -0,0 +1,95 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import org.junit.Test; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +public class ComponentIndexMultipleWordsTest extends ComponentIndexTest { + + @Test + public void should_find_perfect_match() { + assertResultOrder("struts java", + "Struts.java"); + } + + @Test + public void should_find_partial_match() { + features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); + assertResultOrder("struts java", + "Xstrutsx.Xjavax"); + } + + @Test + public void should_find_partial_match_prefix_word1() { + assertResultOrder("struts java", + "MyStruts.java"); + } + + @Test + public void should_find_partial_match_suffix_word1() { + assertResultOrder("struts java", + "StrutsObject.java"); + } + + @Test + public void should_find_partial_match_prefix_word2() { + assertResultOrder("struts java", + "MyStruts.xjava"); + } + + @Test + public void should_find_partial_match_suffix_word2() { + assertResultOrder("struts java", + "MyStruts.javax"); + } + + @Test + public void should_find_partial_match_prefix_and_suffix_everywhere() { + assertResultOrder("struts java", + "MyStrutsObject.xjavax"); + } + + @Test + public void should_find_subset_of_document_terms() { + assertResultOrder("struts java", + "Some.Struts.Class.java.old"); + } + + @Test + public void should_require_all_words_to_match() { + assertNoFileMatches("struts java", + "Struts"); + } + + @Test + public void should_ignore_empty_words() { + assertFileMatches(" struts \n \n\n", + "Struts"); + } + + @Test + public void should_require_all_words_to_match_for_partial() { + features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); + assertNoFileMatches("struts java", + "Struts"); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java new file mode 100644 index 00000000000..de165d98273 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexScoreTest.java @@ -0,0 +1,178 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRepertoire; + +import static java.util.Arrays.asList; +import static org.sonar.api.resources.Qualifiers.FILE; +import static org.sonar.api.resources.Qualifiers.MODULE; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public class ComponentIndexScoreTest extends ComponentIndexTest { + + @Test + public void should_prefer_components_without_prefix() { + assertResultOrder("File.java", + "File.java", + "MyFile.java"); + } + + @Test + public void should_prefer_components_without_suffix() { + assertResultOrder("File", + "File", + "Filex"); + } + + @Test + public void should_prefer_key_matching_over_name_matching() { + ComponentDto project1 = indexProject("quality", "SonarQube"); + ComponentDto project2 = indexProject("sonarqube", "Quality Product"); + + assertExactResults("sonarqube", project2, project1); + } + + @Test + public void should_prefer_prefix_matching_over_partial_matching() { + assertResultOrder("corem", + "CoreMetrics.java", + "ScoreMatrix.java"); + } + + @Test + public void should_prefer_case_sensitive_prefix() { + assertResultOrder("caSe", + "caSeBla.java", + "CaseBla.java"); + } + + @Test + public void scoring_prefix_with_multiple_words() { + assertResultOrder("index java", + "IndexSomething.java", + "MyIndex.java"); + } + + @Test + public void scoring_prefix_with_multiple_words_and_case() { + assertResultOrder("Index JAVA", + "IndexSomething.java", + "index_java.js"); + } + + @Test + public void scoring_long_items() { + assertResultOrder("ThisIsAVeryLongNameToSearchForAndItExceeds15Characters.java", + "ThisIsAVeryLongNameToSearchForAndItExceeds15Characters.java", + "ThisIsAVeryLongNameToSearchForAndItEndsDifferently.java"); + } + + @Test + public void scoring_perfect_match() { + assertResultOrder("SonarQube", + "SonarQube", + "SonarQube SCM Git"); + } + + @Test + public void scoring_perfect_match_dispite_case_changes() { + assertResultOrder("sonarqube", + "SonarQube", + "SonarQube SCM Git"); + } + + @Test + public void scoring_perfect_match_with_matching_case_higher_than_without_matching_case() { + assertResultOrder("sonarqube", + "sonarqube", + "SonarQube"); + } + + @Test + public void should_prefer_favorite_over_recently_browsed() { + ComponentDto file1 = db.components().insertPrivateProject(c -> c.setName("File1")); + index(file1); + + ComponentDto file2 = db.components().insertPrivateProject(c -> c.setName("File2")); + index(file2); + + assertSearch(SuggestionQuery.builder() + .setQuery("File") + .setQualifiers(asList(PROJECT, MODULE, FILE)) + .setRecentlyBrowsedKeys(ImmutableSet.of(file1.getDbKey())) + .setFavoriteKeys(ImmutableSet.of(file2.getDbKey())) + .build()).containsExactly(uuids(file2, file1)); + + assertSearch(SuggestionQuery.builder() + .setQuery("File") + .setQualifiers(asList(PROJECT, MODULE, FILE)) + .setRecentlyBrowsedKeys(ImmutableSet.of(file2.getDbKey())) + .setFavoriteKeys(ImmutableSet.of(file1.getDbKey())) + .build()).containsExactly(uuids(file1, file2)); + } + + @Test + public void do_not_match_wrong_file_extension() { + ComponentDto file1 = indexFile("MyClass.java"); + ComponentDto file2 = indexFile("ClassExample.java"); + ComponentDto file3 = indexFile("Class.java"); + indexFile("Class.cs"); + indexFile("Class.js"); + indexFile("Class.rb"); + + assertExactResults("Class java", file3, file2, file1); + } + + @Test + public void if_relevancy_is_equal_fall_back_to_alphabetical_ordering() { + assertResultOrder("sonarqube", + "sonarqubeA", + "sonarqubeB"); + } + + @Test + public void scoring_test_DbTester() { + features.set(ComponentTextSearchFeatureRepertoire.PARTIAL); + + ComponentDto project = indexProject("key-1", "Quality Product"); + + index(ComponentTesting.newFileDto(project) + .setName("DbTester.java") + .setDbKey("java/org/example/DbTester.java") + .setUuid("UUID-DbTester")); + + index(ComponentTesting.newFileDto(project) + .setName("WebhookDbTesting.java") + .setDbKey("java/org/example/WebhookDbTesting.java") + .setUuid("UUID-WebhookDbTesting")); + + assertSearch("dbt").containsExactly( + + "UUID-DbTester", + "UUID-WebhookDbTesting" + + ); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java new file mode 100644 index 00000000000..46bc6489c71 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexSearchTest.java @@ -0,0 +1,164 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.SearchIdResult; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRule; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.db.component.ComponentTesting.newFileDto; + +public class ComponentIndexSearchTest { + @Rule + public EsTester es = EsTester.create(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ComponentTextSearchFeatureRule features = new ComponentTextSearchFeatureRule(); + + private ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); + + private ComponentIndex underTest = new ComponentIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + + @Test + public void filter_by_language() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto javaFile = db.components().insertComponent(newFileDto(project).setLanguage("java")); + ComponentDto jsFile1 = db.components().insertComponent(newFileDto(project).setLanguage("js")); + ComponentDto jsFile2 = db.components().insertComponent(newFileDto(project).setLanguage("js")); + index(project); + + SearchIdResult result = underTest.search(ComponentQuery.builder().setLanguage("js").build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(jsFile1.uuid(), jsFile2.uuid()); + } + + @Test + public void filter_by_name() { + ComponentDto ignoredProject = db.components().insertPrivateProject(p -> p.setName("ignored project")); + ComponentDto project = db.components().insertPrivateProject(p -> p.setName("Project Shiny name")); + index(ignoredProject, project); + + SearchIdResult result = underTest.search(ComponentQuery.builder().setQuery("shiny").build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); + } + + @Test + public void filter_by_key_with_exact_match() { + ComponentDto ignoredProject = db.components().insertPrivateProject(p -> p.setDbKey("ignored-project")); + ComponentDto project = db.components().insertPrivateProject(p -> p.setDbKey("shiny-project")); + ComponentDto anotherIgnoreProject = db.components().insertPrivateProject(p -> p.setDbKey("another-shiny-project")); + index(ignoredProject, project); + + SearchIdResult result = underTest.search(ComponentQuery.builder().setQuery("shiny-project").build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); + } + + @Test + public void filter_by_qualifier() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + index(project); + + SearchIdResult result = underTest.search(ComponentQuery.builder().setQualifiers(singleton(Qualifiers.FILE)).build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(file.uuid()); + } + + @Test + public void filter_by_organization() { + OrganizationDto organization = db.organizations().insert(); + OrganizationDto anotherOrganization = db.organizations().insert(); + ComponentDto project = db.components().insertPrivateProject(organization); + ComponentDto anotherProject = db.components().insertPrivateProject(anotherOrganization); + index(project, anotherProject); + + SearchIdResult result = underTest.search(ComponentQuery.builder().setOrganization(organization.getUuid()).build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(project.uuid()); + } + + @Test + public void order_by_name_case_insensitive() { + ComponentDto project2 = db.components().insertPrivateProject(p -> p.setName("PROJECT 2")); + ComponentDto project3 = db.components().insertPrivateProject(p -> p.setName("project 3")); + ComponentDto project1 = db.components().insertPrivateProject(p -> p.setName("Project 1")); + index(project1, project2, project3); + + SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactly(project1.uuid(), project2.uuid(), project3.uuid()); + } + + @Test + public void paginate_results() { + List projects = IntStream.range(0, 9) + .mapToObj(i -> db.components().insertPrivateProject(p -> p.setName("project " + i))) + .collect(Collectors.toList()); + index(projects.toArray(new ComponentDto[0])); + + SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions().setPage(2, 3)); + + assertThat(result.getIds()).containsExactlyInAnyOrder(projects.get(3).uuid(), projects.get(4).uuid(), projects.get(5).uuid()); + } + + @Test + public void filter_unauthorized_components() { + ComponentDto unauthorizedProject = db.components().insertPrivateProject(); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + indexer.indexOnStartup(emptySet()); + authorizationIndexerTester.allowOnlyAnyone(project1); + authorizationIndexerTester.allowOnlyAnyone(project2); + + SearchIdResult result = underTest.search(ComponentQuery.builder().build(), new SearchOptions()); + + assertThat(result.getIds()).containsExactlyInAnyOrder(project1.uuid(), project2.uuid()) + .doesNotContain(unauthorizedProject.uuid()); + } + + private void index(ComponentDto... components) { + indexer.indexOnStartup(emptySet()); + Arrays.stream(components).forEach(c -> authorizationIndexerTester.allowOnlyAnyone(c)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java new file mode 100644 index 00000000000..f3c82a27289 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/component/index/ComponentIndexTest.java @@ -0,0 +1,157 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.component.index; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import org.assertj.core.api.ListAssert; +import org.junit.Before; +import org.junit.Rule; +import org.sonar.api.utils.System2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.textsearch.ComponentTextSearchFeatureRule; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.resources.Qualifiers.FILE; +import static org.sonar.api.resources.Qualifiers.MODULE; +import static org.sonar.api.resources.Qualifiers.PROJECT; + +public abstract class ComponentIndexTest { + + @Rule + public EsTester es = EsTester.create(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @Rule + public ComponentTextSearchFeatureRule features = new ComponentTextSearchFeatureRule(); + + protected ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); + protected ComponentIndex index = new ComponentIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + protected PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); + private OrganizationDto organization; + + @Before + public void setUp() { + organization = OrganizationTesting.newOrganizationDto(); + } + + protected void assertFileMatches(String query, String... fileNames) { + ComponentDto[] files = Arrays.stream(fileNames) + .map(this::indexFile) + .toArray(ComponentDto[]::new); + assertSearch(query).containsExactlyInAnyOrder(uuids(files)); + } + + protected void assertNoFileMatches(String query, String... fileNames) { + Arrays.stream(fileNames) + .forEach(this::indexFile); + assertSearch(query).isEmpty(); + } + + protected void assertResultOrder(String query, String... resultsInOrder) { + ComponentDto project = indexProject("key-1", "Quality Product"); + List files = Arrays.stream(resultsInOrder) + .map(r -> ComponentTesting.newFileDto(project).setName(r)) + .peek(f -> f.setUuid(f.uuid() + "_" + f.name().replaceAll("[^a-zA-Z0-9]", ""))) + .collect(Collectors.toList()); + + // index them, but not in the expected order + files.stream() + .sorted(Comparator.comparing(ComponentDto::uuid).reversed()) + .forEach(this::index); + + assertExactResults(query, files.toArray(new ComponentDto[0])); + } + + protected ListAssert assertSearch(String query) { + return assertSearch(SuggestionQuery.builder().setQuery(query).setQualifiers(asList(PROJECT, MODULE, FILE)).build()); + } + + protected ListAssert assertSearch(SuggestionQuery query) { + return (ListAssert)assertThat(index.searchSuggestions(query, features.get()).getQualifiers()) + .flatExtracting(ComponentHitsPerQualifier::getHits) + .extracting(ComponentHit::getUuid); + } + + protected void assertSearchResults(String query, ComponentDto... expectedComponents) { + assertSearchResults(SuggestionQuery.builder().setQuery(query).setQualifiers(asList(PROJECT, MODULE, FILE)).build(), expectedComponents); + } + + protected void assertSearchResults(SuggestionQuery query, ComponentDto... expectedComponents) { + assertSearch(query).containsOnly(uuids(expectedComponents)); + } + + protected void assertExactResults(String query, ComponentDto... expectedComponents) { + assertSearch(query).containsExactly(uuids(expectedComponents)); + } + + protected void assertNoSearchResults(String query) { + assertSearchResults(query); + } + + protected ComponentDto indexProject(String key, String name) { + return index( + ComponentTesting.newPrivateProjectDto(organization, "UUID_" + key) + .setDbKey(key) + .setName(name)); + } + + protected ComponentDto newProject(String key, String name) { + return ComponentTesting.newPrivateProjectDto(organization, "UUID_" + key) + .setDbKey(key) + .setName(name); + } + + protected ComponentDto indexFile(String fileName) { + ComponentDto project = indexProject("key-1", "SonarQube"); + return indexFile(project, "src/main/java/" + fileName, fileName); + } + + protected ComponentDto indexFile(ComponentDto project, String fileKey, String fileName) { + return index( + ComponentTesting.newFileDto(project) + .setDbKey(fileKey) + .setName(fileName)); + } + + protected ComponentDto index(ComponentDto dto) { + indexer.index(dto); + authorizationIndexerTester.allowOnlyAnyone(dto); + return dto; + } + + protected static String[] uuids(ComponentDto... expectedComponents) { + return Arrays.stream(expectedComponents).map(ComponentDto::uuid).toArray(String[]::new); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java index 110092b5e16..e95d2ebcb96 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchActionTest.java @@ -43,8 +43,8 @@ import org.sonar.server.component.ws.SearchAction.SearchRequest; import org.sonar.server.es.EsTester; import org.sonar.server.l18n.I18nRule; import org.sonar.server.organization.TestDefaultOrganizationProvider; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; @@ -93,7 +93,7 @@ public class SearchActionTest { private Languages languages = mock(Languages.class); private ComponentIndexer indexer = new ComponentIndexer(db.getDbClient(), es.client()); private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, indexer); - private ComponentIndex index = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); + private ComponentIndex index = new ComponentIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); private UserDto user; diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java index a91189f110c..23bd0f1070f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SearchProjectsActionTest.java @@ -49,8 +49,8 @@ import org.sonar.server.component.ws.SearchProjectsAction.SearchProjectsRequest; import org.sonar.server.es.EsTester; import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresIndexer; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; @@ -125,7 +125,7 @@ public class SearchProjectsActionTest { private DbSession dbSession = db.getSession(); private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, new ProjectMeasuresIndexer(dbClient, es.client())); - private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); + private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); private ProjectMeasuresIndexer projectMeasuresIndexer = new ProjectMeasuresIndexer(db.getDbClient(), es.client()); private WsActionTester ws = new WsActionTester( diff --git a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java index b3024b62fe1..325784e3c55 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/component/ws/SuggestionsActionTest.java @@ -41,8 +41,8 @@ import org.sonar.server.component.index.ComponentIndex; import org.sonar.server.component.index.ComponentIndexer; import org.sonar.server.es.EsTester; import org.sonar.server.favorite.FavoriteFinder; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.TestResponse; @@ -96,7 +96,7 @@ public class SuggestionsActionTest { private ComponentIndexer componentIndexer = new ComponentIndexer(db.getDbClient(), es.client()); private FavoriteFinder favoriteFinder = mock(FavoriteFinder.class); - private ComponentIndex index = new ComponentIndex(es.client(), new AuthorizationTypeSupport(userSessionRule), System2.INSTANCE); + private ComponentIndex index = new ComponentIndex(es.client(), new WebAuthorizationTypeSupport(userSessionRule), System2.INSTANCE); private SuggestionsAction underTest = new SuggestionsAction(db.getDbClient(), index, favoriteFinder, userSessionRule, resourceTypes); private OrganizationDto organization; private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, componentIndexer); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/IssuesFinderSortTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/IssuesFinderSortTest.java index 93f4380b47e..c7334911c4f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/IssuesFinderSortTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/IssuesFinderSortTest.java @@ -24,6 +24,7 @@ import java.util.List; import org.apache.commons.lang.time.DateUtils; import org.junit.Test; import org.sonar.db.issue.IssueDto; +import org.sonar.server.issue.index.IssueQuery; import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java new file mode 100644 index 00000000000..3cafe683b75 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexDebtTest.java @@ -0,0 +1,296 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import java.util.Map; +import java.util.TimeZone; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.issue.IssueDocTesting; +import org.sonar.server.issue.index.IssueQuery.Builder; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.api.issue.Issue.STATUS_CLOSED; +import static org.sonar.api.issue.Issue.STATUS_OPEN; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.DEPRECATED_FACET_MODE_DEBT; +import static org.sonarqube.ws.client.issue.IssuesWsParameters.FACET_MODE_EFFORT; + +public class IssueIndexDebtTest { + + private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(TimeZone.getTimeZone("GMT-01:00")); + + @Rule + public EsTester es = EsTester.create(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @Rule + public DbTester db = DbTester.create(system2); + + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); + private IssueIndex underTest; + + @Before + public void setUp() { + underTest = new IssueIndex(es.client(), system2, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); + } + + @Test + public void facets_on_projects() { + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto, "ABCD"); + ComponentDto project2 = ComponentTesting.newPrivateProjectDto(organizationDto, "EFGH"); + + indexIssues( + IssueDocTesting.newDoc("I1", ComponentTesting.newFileDto(project, null)).setEffort(10L), + IssueDocTesting.newDoc("I2", ComponentTesting.newFileDto(project, null)).setEffort(10L), + IssueDocTesting.newDoc("I3", ComponentTesting.newFileDto(project2, null)).setEffort(10L)); + + Facets facets = search("projectUuids"); + assertThat(facets.getNames()).containsOnly("projectUuids", FACET_MODE_EFFORT); + assertThat(facets.get("projectUuids")).containsOnly(entry("ABCD", 20L), entry("EFGH", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); + } + + @Test + public void facets_on_components() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto(), "A"); + ComponentDto file1 = ComponentTesting.newFileDto(project, null, "ABCD"); + ComponentDto file2 = ComponentTesting.newFileDto(project, null, "BCDE"); + ComponentDto file3 = ComponentTesting.newFileDto(project, null, "CDEF"); + + indexIssues( + IssueDocTesting.newDoc("I1", project).setEffort(10L), + IssueDocTesting.newDoc("I2", file1).setEffort(10L), + IssueDocTesting.newDoc("I3", file2).setEffort(10L), + IssueDocTesting.newDoc("I4", file2).setEffort(10L), + IssueDocTesting.newDoc("I5", file3).setEffort(10L)); + + Facets facets = search("fileUuids"); + assertThat(facets.getNames()).containsOnly("fileUuids", FACET_MODE_EFFORT); + assertThat(facets.get("fileUuids")) + .containsOnly(entry("A", 10L), entry("ABCD", 10L), entry("BCDE", 20L), entry("CDEF", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 50L)); + } + + @Test + public void facets_on_directories() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file1 = ComponentTesting.newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); + ComponentDto file2 = ComponentTesting.newFileDto(project, null).setPath("F2.xoo"); + + indexIssues( + IssueDocTesting.newDoc("I1", file1).setDirectoryPath("/src/main/xoo").setEffort(10L), + IssueDocTesting.newDoc("I2", file2).setDirectoryPath("/").setEffort(10L)); + + Facets facets = search("directories"); + assertThat(facets.getNames()).containsOnly("directories", FACET_MODE_EFFORT); + assertThat(facets.get("directories")).containsOnly(entry("/src/main/xoo", 10L), entry("/", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 20L)); + } + + @Test + public void facets_on_severities() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + indexIssues( + IssueDocTesting.newDoc("I1", file).setSeverity(Severity.INFO).setEffort(10L), + IssueDocTesting.newDoc("I2", file).setSeverity(Severity.INFO).setEffort(10L), + IssueDocTesting.newDoc("I3", file).setSeverity(Severity.MAJOR).setEffort(10L)); + + Facets facets = search("severities"); + assertThat(facets.getNames()).containsOnly("severities", FACET_MODE_EFFORT); + assertThat(facets.get("severities")).containsOnly(entry("INFO", 20L), entry("MAJOR", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); + } + + @Test + public void facets_on_statuses() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + indexIssues( + IssueDocTesting.newDoc("I1", file).setStatus(STATUS_CLOSED).setEffort(10L), + IssueDocTesting.newDoc("I2", file).setStatus(STATUS_CLOSED).setEffort(10L), + IssueDocTesting.newDoc("I3", file).setStatus(STATUS_OPEN).setEffort(10L)); + + Facets facets = search("statuses"); + assertThat(facets.getNames()).containsOnly("statuses", FACET_MODE_EFFORT); + assertThat(facets.get("statuses")).containsOnly(entry("CLOSED", 20L), entry("OPEN", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); + } + + @Test + public void facets_on_resolutions() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + indexIssues( + IssueDocTesting.newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setEffort(10L), + IssueDocTesting.newDoc("I2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setEffort(10L), + IssueDocTesting.newDoc("I3", file).setResolution(Issue.RESOLUTION_FIXED).setEffort(10L)); + + Facets facets = search("resolutions"); + assertThat(facets.getNames()).containsOnly("resolutions", FACET_MODE_EFFORT); + assertThat(facets.get("resolutions")).containsOnly(entry("FALSE-POSITIVE", 20L), entry("FIXED", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); + } + + @Test + public void facets_on_languages() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + RuleKey ruleKey = RuleKey.of("repo", "X1"); + + indexIssues(IssueDocTesting.newDoc("I1", file).setLanguage("xoo").setEffort(10L)); + + Facets facets = search("languages"); + assertThat(facets.getNames()).containsOnly("languages", FACET_MODE_EFFORT); + assertThat(facets.get("languages")).containsOnly(entry("xoo", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 10L)); + } + + private Facets search(String additionalFacet) { + return new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList(additionalFacet))), system2.getDefaultTimeZone()); + } + + @Test + public void facets_on_assignees() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + indexIssues( + IssueDocTesting.newDoc("I1", file).setAssigneeUuid("uuid-steph").setEffort(10L), + IssueDocTesting.newDoc("I2", file).setAssigneeUuid("uuid-simon").setEffort(10L), + IssueDocTesting.newDoc("I3", file).setAssigneeUuid("uuid-simon").setEffort(10L), + IssueDocTesting.newDoc("I4", file).setAssigneeUuid(null).setEffort(10L)); + + Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("assignees"))), system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly("assignees", FACET_MODE_EFFORT); + assertThat(facets.get("assignees")).containsOnly(entry("uuid-steph", 10L), entry("uuid-simon", 20L), entry("", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L)); + } + + @Test + public void facets_on_authors() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + indexIssues( + IssueDocTesting.newDoc("I1", file).setAuthorLogin("steph").setEffort(10L), + IssueDocTesting.newDoc("I2", file).setAuthorLogin("simon").setEffort(10L), + IssueDocTesting.newDoc("I3", file).setAuthorLogin("simon").setEffort(10L), + IssueDocTesting.newDoc("I4", file).setAuthorLogin(null).setEffort(10L)); + + Facets facets = new Facets(underTest.search(newQueryBuilder().build(), new SearchOptions().addFacets(asList("authors"))), system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly("authors", FACET_MODE_EFFORT); + assertThat(facets.get("authors")).containsOnly(entry("steph", 10L), entry("simon", 20L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 40L)); + } + + @Test + public void facet_on_created_at() { + SearchOptions searchOptions = fixtureForCreatedAtFacet(); + + Builder query = newQueryBuilder().createdBefore(parseDateTime("2016-01-01T00:00:00+0100")); + Map createdAt = new Facets(underTest.search(query.build(), searchOptions), system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2011-01-01", 10L), + entry("2012-01-01", 0L), + entry("2013-01-01", 0L), + entry("2014-01-01", 50L), + entry("2015-01-01", 10L)); + } + + @Test + public void deprecated_debt_facets() { + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto, "ABCD"); + ComponentDto project2 = ComponentTesting.newPrivateProjectDto(organizationDto, "EFGH"); + + indexIssues( + IssueDocTesting.newDoc("I1", ComponentTesting.newFileDto(project, null)).setEffort(10L), + IssueDocTesting.newDoc("I2", ComponentTesting.newFileDto(project, null)).setEffort(10L), + IssueDocTesting.newDoc("I3", ComponentTesting.newFileDto(project2, null)).setEffort(10L)); + + Facets facets = new Facets(underTest.search(IssueQuery.builder().facetMode(DEPRECATED_FACET_MODE_DEBT).build(), + new SearchOptions().addFacets(asList("projectUuids"))), system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly("projectUuids", FACET_MODE_EFFORT); + assertThat(facets.get("projectUuids")).containsOnly(entry("ABCD", 20L), entry("EFGH", 10L)); + assertThat(facets.get(FACET_MODE_EFFORT)).containsOnly(entry("total", 30L)); + } + + private SearchOptions fixtureForCreatedAtFacet() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = ComponentTesting.newFileDto(project, null); + + IssueDoc issue0 = IssueDocTesting.newDoc("ISSUE0", file).setEffort(10L).setFuncCreationDate(parseDateTime("2011-04-25T01:05:13+0100")); + IssueDoc issue1 = IssueDocTesting.newDoc("I1", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-01T12:34:56+0100")); + IssueDoc issue2 = IssueDocTesting.newDoc("I2", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-01T23:46:00+0100")); + IssueDoc issue3 = IssueDocTesting.newDoc("I3", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-02T12:34:56+0100")); + IssueDoc issue4 = IssueDocTesting.newDoc("I4", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-05T12:34:56+0100")); + IssueDoc issue5 = IssueDocTesting.newDoc("I5", file).setEffort(10L).setFuncCreationDate(parseDateTime("2014-09-20T12:34:56+0100")); + IssueDoc issue6 = IssueDocTesting.newDoc("I6", file).setEffort(10L).setFuncCreationDate(parseDateTime("2015-01-18T12:34:56+0100")); + + indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6); + + return new SearchOptions().addFacets("createdAt"); + } + + private void indexIssues(IssueDoc... issues) { + issueIndexer.index(asList(issues).iterator()); + for (IssueDoc issue : issues) { + addIssueAuthorization(issue.projectUuid()); + } + } + + private void addIssueAuthorization(String projectUuid) { + IndexPermissions access = new IndexPermissions(projectUuid, Qualifiers.PROJECT); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + } + + private Builder newQueryBuilder() { + return IssueQuery.builder().facetMode(FACET_MODE_EFFORT); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java new file mode 100644 index 00000000000..b32479cea3d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexProjectStatisticsTest.java @@ -0,0 +1,271 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import java.util.Date; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.utils.System2; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.mock; +import static org.sonar.db.component.ComponentTesting.newBranchDto; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.component.ComponentTesting.newProjectBranch; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; +import static org.sonar.server.issue.IssueDocTesting.newDoc; + +public class IssueIndexProjectStatisticsTest { + + private System2 system2 = mock(System2.class); + @Rule + public EsTester es = EsTester.create(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), null, new IssueIteratorFactory(null)); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); + + private IssueIndex underTest = new IssueIndex(es.client(), system2, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); + + @Test + public void searchProjectStatistics_returns_empty_list_if_no_input() { + List result = underTest.searchProjectStatistics(emptyList(), emptyList(), "unknownUser"); + assertThat(result).isEmpty(); + } + + @Test + public void searchProjectStatistics_returns_empty_list_if_the_input_does_not_match_anything() { + List result = underTest.searchProjectStatistics(singletonList("unknownProjectUuid"), singletonList(1_111_234_567_890L), "unknownUser"); + assertThat(result).isEmpty(); + } + + @Test + public void searchProjectStatistics_returns_something() { + OrganizationDto organization = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(organization); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); + } + + @Test + public void searchProjectStatistics_does_not_return_results_if_assignee_does_not_match() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String user1Uuid = randomAlphanumeric(40); + String user2Uuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(user1Uuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), user2Uuid); + + assertThat(result).isEmpty(); + } + + @Test + public void searchProjectStatistics_returns_results_if_assignee_matches() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String user1Uuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(user1Uuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), user1Uuid); + + assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); + } + + @Test + public void searchProjectStatistics_returns_results_if_functional_date_is_strictly_after_from_date() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); + } + + @Test + public void searchProjectStatistics_does_not_return_results_if_functional_date_is_same_as_from_date() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).extracting(ProjectStatistics::getProjectUuid).containsExactly(project.uuid()); + } + + @Test + public void searchProjectStatistics_does_not_return_resolved_issues() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues( + newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), + newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_FIXED), + newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_REMOVED), + newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)).setResolution(Issue.RESOLUTION_WONT_FIX)); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).isEmpty(); + } + + @Test + public void searchProjectStatistics_does_not_return_results_if_functional_date_is_before_from_date() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues(newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from - 1000L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).isEmpty(); + } + + @Test + public void searchProjectStatistics_returns_issue_count() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues( + newDoc("issue1", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue2", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue3", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result).extracting(ProjectStatistics::getIssueCount).containsExactly(3L); + } + + @Test + public void searchProjectStatistics_returns_issue_count_for_multiple_projects() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(org1); + ComponentDto project2 = newPrivateProjectDto(org1); + ComponentDto project3 = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues( + newDoc("issue1", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue2", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue3", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + + newDoc("issue4", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue5", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics( + asList(project1.uuid(), project2.uuid(), project3.uuid()), + asList(from, from, from), + userUuid); + + assertThat(result) + .extracting(ProjectStatistics::getProjectUuid, ProjectStatistics::getIssueCount) + .containsExactlyInAnyOrder( + tuple(project1.uuid(), 3L), + tuple(project3.uuid(), 2L)); + } + + @Test + public void searchProjectStatistics_returns_max_date_for_multiple_projects() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(org1); + ComponentDto project2 = newPrivateProjectDto(org1); + ComponentDto project3 = newPrivateProjectDto(org1); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_000L; + indexIssues( + newDoc("issue1", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1_000L)), + newDoc("issue2", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 2_000L)), + newDoc("issue3", project1).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 3_000L)), + + newDoc("issue4", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 4_000L)), + newDoc("issue5", project3).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 5_000L))); + + List result = underTest.searchProjectStatistics( + asList(project1.uuid(), project2.uuid(), project3.uuid()), + asList(from, from, from), + userUuid); + + assertThat(result) + .extracting(ProjectStatistics::getProjectUuid, ProjectStatistics::getLastIssueDate) + .containsExactlyInAnyOrder( + tuple(project1.uuid(), from + 3_000L), + tuple(project3.uuid(), from + 5_000L)); + } + + @Test + public void searchProjectStatistics_return_branch_issues() { + OrganizationDto organization = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(organization); + ComponentDto branch = newProjectBranch(project, newBranchDto(project).setKey("branch")); + String userUuid = randomAlphanumeric(40); + long from = 1_111_234_567_890L; + indexIssues( + newDoc("issue1", branch).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L)), + newDoc("issue2", branch).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 2L)), + newDoc("issue3", project).setAssigneeUuid(userUuid).setFuncCreationDate(new Date(from + 1L))); + + List result = underTest.searchProjectStatistics(singletonList(project.uuid()), singletonList(from), userUuid); + + assertThat(result) + .extracting(ProjectStatistics::getIssueCount, ProjectStatistics::getProjectUuid, ProjectStatistics::getLastIssueDate) + .containsExactly( + tuple(2L, branch.uuid(), from + 2L), + tuple(1L, project.uuid(), from + 1L)); + } + + private void indexIssues(IssueDoc... issues) { + issueIndexer.index(asList(issues).iterator()); + for (IssueDoc issue : issues) { + IndexPermissions access = new IndexPermissions(issue.projectUuid(), "TRK"); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java new file mode 100644 index 00000000000..cc1f50f393e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueIndexTest.java @@ -0,0 +1,1752 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterators; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.assertj.core.api.Fail; +import org.assertj.core.groups.Tuple; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.search.SearchHit; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.issue.Issue; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.System2; +import org.sonar.api.utils.internal.TestSystem2; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.rule.index.RuleIndexer; +import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.view.index.ViewDoc; +import org.sonar.server.view.index.ViewIndexer; + +import static com.google.common.collect.ImmutableSortedSet.of; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.TimeZone.getTimeZone; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.api.Assertions.tuple; +import static org.junit.rules.ExpectedException.none; +import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; +import static org.sonar.api.resources.Qualifiers.APP; +import static org.sonar.api.rules.RuleType.BUG; +import static org.sonar.api.rules.RuleType.CODE_SMELL; +import static org.sonar.api.rules.RuleType.VULNERABILITY; +import static org.sonar.api.utils.DateUtils.addDays; +import static org.sonar.api.utils.DateUtils.parseDate; +import static org.sonar.api.utils.DateUtils.parseDateTime; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newModuleDto; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; +import static org.sonar.db.rule.RuleTesting.newRule; +import static org.sonar.db.user.GroupTesting.newGroupDto; +import static org.sonar.db.user.UserTesting.newUserDto; +import static org.sonar.server.issue.IssueDocTesting.newDoc; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_INSECURE_INTERACTION; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_POROUS_DEFENSES; +import static org.sonar.server.issue.index.IssueIndexDefinition.SANS_TOP_25_RISKY_RESOURCE; +import static org.sonar.server.issue.index.IssueIndexDefinition.UNKNOWN_STANDARD; + +public class IssueIndexTest { + + @Rule + public EsTester es = EsTester.create(); + @Rule + public UserSessionRule userSessionRule = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = none(); + private System2 system2 = new TestSystem2().setNow(1_500_000_000_000L).setDefaultTimeZone(getTimeZone("GMT-01:00")); + @Rule + public DbTester db = DbTester.create(system2); + + private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); + private ViewIndexer viewIndexer = new ViewIndexer(db.getDbClient(), es.client()); + private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), db.getDbClient()); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, issueIndexer); + + private IssueIndex underTest = new IssueIndex(es.client(), system2, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); + + @Test + public void filter_by_keys() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + + indexIssues( + newDoc("I1", newFileDto(project, null)), + newDoc("I2", newFileDto(project, null))); + + assertThatSearchReturnsOnly(IssueQuery.builder().issueKeys(asList("I1", "I2")), "I1", "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().issueKeys(singletonList("I1")), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().issueKeys(asList("I3", "I4"))); + } + + @Test + public void filter_by_projects() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto module = newModuleDto(project); + ComponentDto subModule = newModuleDto(module); + + indexIssues( + newDoc("I1", project), + newDoc("I2", newFileDto(project, null)), + newDoc("I3", module), + newDoc("I4", newFileDto(module, null)), + newDoc("I5", subModule), + newDoc("I6", newFileDto(subModule, null))); + + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); + } + + @Test + public void facet_on_projectUuids() { + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(organizationDto, "ABCD"); + ComponentDto project2 = newPrivateProjectDto(organizationDto, "EFGH"); + + indexIssues( + newDoc("I1", newFileDto(project, null)), + newDoc("I2", newFileDto(project, null)), + newDoc("I3", newFileDto(project2, null))); + + assertThatFacetHasExactly(IssueQuery.builder(), "projectUuids", entry("ABCD", 2L), entry("EFGH", 1L)); + } + + @Test + public void filter_by_modules() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto module = newModuleDto(project); + ComponentDto subModule = newModuleDto(module); + ComponentDto file = newFileDto(subModule, null); + + indexIssues( + newDoc("I3", module), + newDoc("I5", subModule), + newDoc("I2", file)); + + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(file.uuid()))); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(module.uuid())), "I3"); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(subModule.uuid())), "I2", "I5"); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList(project.uuid()))); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project.uuid())).moduleUuids(singletonList("unknown"))); + } + + @Test + public void filter_by_components_on_contextualized_search() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto module = newModuleDto(project); + ComponentDto subModule = newModuleDto(module); + ComponentDto file1 = newFileDto(project, null); + ComponentDto file2 = newFileDto(module, null); + ComponentDto file3 = newFileDto(subModule, null); + String view = "ABCD"; + indexView(view, asList(project.uuid())); + + indexIssues( + newDoc("I1", project), + newDoc("I2", file1), + newDoc("I3", module), + newDoc("I4", file2), + newDoc("I5", subModule), + newDoc("I6", file3)); + + assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(asList(file1.uuid(), file2.uuid(), file3.uuid())), "I2", "I4", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(file1.uuid())), "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().moduleRootUuids(singletonList(subModule.uuid())), "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().moduleRootUuids(singletonList(module.uuid())), "I3", "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view)), "I1", "I2", "I3", "I4", "I5", "I6"); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); + } + + @Test + public void filter_by_components_on_non_contextualized_search() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto(), "project"); + ComponentDto file1 = newFileDto(project, null, "file1"); + ComponentDto module = newModuleDto(project).setUuid("module"); + ComponentDto file2 = newFileDto(module, null, "file2"); + ComponentDto subModule = newModuleDto(module).setUuid("subModule"); + ComponentDto file3 = newFileDto(subModule, null, "file3"); + String view = "ABCD"; + indexView(view, asList(project.uuid())); + + indexIssues( + newDoc("I1", project), + newDoc("I2", file1), + newDoc("I3", module), + newDoc("I4", file2), + newDoc("I5", subModule), + newDoc("I6", file3)); + + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList("unknown"))); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())), "I1", "I2", "I3", "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view)), "I1", "I2", "I3", "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(module.uuid())), "I3", "I4"); + assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(subModule.uuid())), "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(file1.uuid())), "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(asList(file1.uuid(), file2.uuid(), file3.uuid())), "I2", "I4", "I6"); + } + + @Test + public void facets_on_components() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto(), "A"); + ComponentDto file1 = newFileDto(project, null, "ABCD"); + ComponentDto file2 = newFileDto(project, null, "BCDE"); + ComponentDto file3 = newFileDto(project, null, "CDEF"); + + indexIssues( + newDoc("I1", project), + newDoc("I2", file1), + newDoc("I3", file2), + newDoc("I4", file2), + newDoc("I5", file3)); + + assertThatFacetHasOnly(IssueQuery.builder(), "fileUuids", entry("A", 1L), entry("ABCD", 1L), entry("BCDE", 2L), entry("CDEF", 1L)); + } + + @Test + public void filter_by_directories() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file1 = newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); + ComponentDto file2 = newFileDto(project, null).setPath("F2.xoo"); + + indexIssues( + newDoc("I1", file1).setDirectoryPath("/src/main/xoo"), + newDoc("I2", file2).setDirectoryPath("/")); + + assertThatSearchReturnsOnly(IssueQuery.builder().directories(singletonList("/src/main/xoo")), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().directories(singletonList("/")), "I2"); + assertThatSearchReturnsEmpty(IssueQuery.builder().directories(singletonList("unknown"))); + } + + @Test + public void facets_on_directories() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file1 = newFileDto(project, null).setPath("src/main/xoo/F1.xoo"); + ComponentDto file2 = newFileDto(project, null).setPath("F2.xoo"); + + indexIssues( + newDoc("I1", file1).setDirectoryPath("/src/main/xoo"), + newDoc("I2", file2).setDirectoryPath("/")); + + assertThatFacetHasOnly(IssueQuery.builder(), "directories", entry("/src/main/xoo", 1L), entry("/", 1L)); + } + + @Test + public void filter_by_portfolios() { + ComponentDto portfolio1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto portfolio2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project1)); + ComponentDto project2 = db.components().insertPrivateProject(); + + IssueDoc issueOnProject1 = newDoc(project1); + IssueDoc issueOnFile = newDoc(file); + IssueDoc issueOnProject2 = newDoc(project2); + + indexIssues(issueOnProject1, issueOnFile, issueOnProject2); + indexView(portfolio1.uuid(), singletonList(project1.uuid())); + indexView(portfolio2.uuid(), singletonList(project2.uuid())); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())), issueOnProject1.key(), issueOnFile.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio2.uuid())), issueOnProject2.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(asList(portfolio1.uuid(), portfolio2.uuid())), issueOnProject1.key(), issueOnFile.key(), issueOnProject2.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())).projectUuids(singletonList(project1.uuid())), issueOnProject1.key(), + issueOnFile.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio1.uuid())).fileUuids(singletonList(file.uuid())), issueOnFile.key()); + assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList("unknown"))); + } + + @Test + public void filter_by_portfolios_not_having_projects() { + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(organizationDto); + ComponentDto file1 = newFileDto(project1, null); + indexIssues(newDoc("I2", file1)); + String view1 = "ABCD"; + indexView(view1, emptyList()); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(view1))); + } + + @Test + public void do_not_return_issues_from_project_branch_when_filtering_by_portfolios() { + ComponentDto portfolio = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto project = db.components().insertMainBranch(); + ComponentDto projectBranch = db.components().insertProjectBranch(project); + ComponentDto fileOnProjectBranch = db.components().insertComponent(newFileDto(projectBranch)); + indexView(portfolio.uuid(), singletonList(project.uuid())); + + IssueDoc issueOnProject = newDoc(project); + IssueDoc issueOnProjectBranch = newDoc(projectBranch); + IssueDoc issueOnFileOnProjectBranch = newDoc(fileOnProjectBranch); + indexIssues(issueOnProject, issueOnFileOnProjectBranch, issueOnProjectBranch); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())), issueOnProject.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())).projectUuids(singletonList(project.uuid())), + issueOnProject.key()); + assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList(portfolio.uuid())).projectUuids(singletonList(projectBranch.uuid()))); + } + + @Test + public void filter_one_issue_by_project_and_branch() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + ComponentDto anotherbBranch = db.components().insertProjectBranch(project); + + IssueDoc issueOnProject = newDoc(project); + IssueDoc issueOnBranch = newDoc(branch); + IssueDoc issueOnAnotherBranch = newDoc(anotherbBranch); + indexIssues(issueOnProject, issueOnBranch, issueOnAnotherBranch); + + assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().componentUuids(singletonList(branch.uuid())).branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).branchUuid(branch.uuid()).mainBranch(false), issueOnBranch.key()); + assertThatSearchReturnsOnly( + IssueQuery.builder().componentUuids(singletonList(branch.uuid())).projectUuids(singletonList(project.uuid())).branchUuid(branch.uuid()).mainBranch(false), + issueOnBranch.key()); + assertThatSearchReturnsEmpty(IssueQuery.builder().branchUuid("unknown")); + } + + @Test + public void issues_from_branch_component_children() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto projectModule = db.components().insertComponent(newModuleDto(project)); + ComponentDto projectFile = db.components().insertComponent(newFileDto(projectModule)); + ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); + ComponentDto branchModule = db.components().insertComponent(newModuleDto(branch)); + ComponentDto branchFile = db.components().insertComponent(newFileDto(branchModule)); + + indexIssues( + newDoc("I1", project), + newDoc("I2", projectFile), + newDoc("I3", projectModule), + newDoc("I4", branch), + newDoc("I5", branchModule), + newDoc("I6", branchFile)); + + assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(branch.uuid()).mainBranch(false), "I4", "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().moduleUuids(singletonList(branchModule.uuid())).branchUuid(branch.uuid()).mainBranch(false), "I5", "I6"); + assertThatSearchReturnsOnly(IssueQuery.builder().fileUuids(singletonList(branchFile.uuid())).branchUuid(branch.uuid()).mainBranch(false), "I6"); + assertThatSearchReturnsEmpty(IssueQuery.builder().fileUuids(singletonList(branchFile.uuid())).mainBranch(false).branchUuid("unknown")); + } + + @Test + public void issues_from_main_branch() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + + IssueDoc issueOnProject = newDoc(project); + IssueDoc issueOnBranch = newDoc(branch); + indexIssues(issueOnProject, issueOnBranch); + + assertThatSearchReturnsOnly(IssueQuery.builder().branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().componentUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().projectUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), issueOnProject.key()); + assertThatSearchReturnsOnly( + IssueQuery.builder().componentUuids(singletonList(project.uuid())).projectUuids(singletonList(project.uuid())).branchUuid(project.uuid()).mainBranch(true), + issueOnProject.key()); + } + + @Test + public void branch_issues_are_ignored_when_no_branch_param() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setKey("my_branch")); + + IssueDoc projectIssue = newDoc(project); + IssueDoc branchIssue = newDoc(branch); + indexIssues(projectIssue, branchIssue); + + assertThatSearchReturnsOnly(IssueQuery.builder(), projectIssue.key()); + } + + @Test + public void filter_by_main_application() { + ComponentDto application1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto application2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project1)); + ComponentDto project2 = db.components().insertPrivateProject(); + indexView(application1.uuid(), singletonList(project1.uuid())); + indexView(application2.uuid(), singletonList(project2.uuid())); + + IssueDoc issueOnProject1 = newDoc(project1); + IssueDoc issueOnFile = newDoc(file); + IssueDoc issueOnProject2 = newDoc(project2); + indexIssues(issueOnProject1, issueOnFile, issueOnProject2); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())), issueOnProject1.key(), issueOnFile.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application2.uuid())), issueOnProject2.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(asList(application1.uuid(), application2.uuid())), issueOnProject1.key(), issueOnFile.key(), issueOnProject2.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())).projectUuids(singletonList(project1.uuid())), issueOnProject1.key(), + issueOnFile.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(application1.uuid())).fileUuids(singletonList(file.uuid())), issueOnFile.key()); + assertThatSearchReturnsEmpty(IssueQuery.builder().viewUuids(singletonList("unknown"))); + } + + @Test + public void filter_by_application_branch() { + ComponentDto application = db.components().insertMainBranch(c -> c.setQualifier(APP)); + ComponentDto branch1 = db.components().insertProjectBranch(application); + ComponentDto branch2 = db.components().insertProjectBranch(application); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project1)); + ComponentDto project2 = db.components().insertPrivateProject(); + indexView(branch1.uuid(), singletonList(project1.uuid())); + indexView(branch2.uuid(), singletonList(project2.uuid())); + + IssueDoc issueOnProject1 = newDoc(project1); + IssueDoc issueOnFile = newDoc(file); + IssueDoc issueOnProject2 = newDoc(project2); + indexIssues(issueOnProject1, issueOnFile, issueOnProject2); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).branchUuid(branch1.uuid()).mainBranch(false), + issueOnProject1.key(), issueOnFile.key()); + assertThatSearchReturnsOnly( + IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).projectUuids(singletonList(project1.uuid())).branchUuid(branch1.uuid()).mainBranch(false), + issueOnProject1.key(), issueOnFile.key()); + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(branch1.uuid())).fileUuids(singletonList(file.uuid())).branchUuid(branch1.uuid()).mainBranch(false), + issueOnFile.key()); + assertThatSearchReturnsEmpty(IssueQuery.builder().branchUuid("unknown")); + } + + @Test + public void filter_by_application_branch_having_project_branches() { + ComponentDto application = db.components().insertMainBranch(c -> c.setQualifier(APP).setDbKey("app")); + ComponentDto applicationBranch1 = db.components().insertProjectBranch(application, a -> a.setKey("app-branch1")); + ComponentDto applicationBranch2 = db.components().insertProjectBranch(application, a -> a.setKey("app-branch2")); + ComponentDto project1 = db.components().insertPrivateProject(p -> p.setDbKey("prj1")); + ComponentDto project1Branch1 = db.components().insertProjectBranch(project1); + ComponentDto fileOnProject1Branch1 = db.components().insertComponent(newFileDto(project1Branch1)); + ComponentDto project1Branch2 = db.components().insertProjectBranch(project1); + ComponentDto project2 = db.components().insertPrivateProject(p -> p.setDbKey("prj2")); + indexView(applicationBranch1.uuid(), asList(project1Branch1.uuid(), project2.uuid())); + indexView(applicationBranch2.uuid(), singletonList(project1Branch2.uuid())); + + IssueDoc issueOnProject1 = newDoc(project1); + IssueDoc issueOnProject1Branch1 = newDoc(project1Branch1); + IssueDoc issueOnFileOnProject1Branch1 = newDoc(fileOnProject1Branch1); + IssueDoc issueOnProject1Branch2 = newDoc(project1Branch2); + IssueDoc issueOnProject2 = newDoc(project2); + indexIssues(issueOnProject1, issueOnProject1Branch1, issueOnFileOnProject1Branch1, issueOnProject1Branch2, issueOnProject2); + + assertThatSearchReturnsOnly(IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).branchUuid(applicationBranch1.uuid()).mainBranch(false), + issueOnProject1Branch1.key(), issueOnFileOnProject1Branch1.key(), issueOnProject2.key()); + assertThatSearchReturnsOnly( + IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).projectUuids(singletonList(project1.uuid())).branchUuid(applicationBranch1.uuid()).mainBranch(false), + issueOnProject1Branch1.key(), issueOnFileOnProject1Branch1.key()); + assertThatSearchReturnsOnly( + IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).fileUuids(singletonList(fileOnProject1Branch1.uuid())).branchUuid(applicationBranch1.uuid()) + .mainBranch(false), + issueOnFileOnProject1Branch1.key()); + assertThatSearchReturnsEmpty( + IssueQuery.builder().viewUuids(singletonList(applicationBranch1.uuid())).projectUuids(singletonList("unknown")).branchUuid(applicationBranch1.uuid()).mainBranch(false)); + } + + @Test + public void filter_by_created_after_by_projects() { + Date now = new Date(); + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(organizationDto); + IssueDoc project1Issue1 = newDoc(project1).setFuncCreationDate(addDays(now, -10)); + IssueDoc project1Issue2 = newDoc(project1).setFuncCreationDate(addDays(now, -20)); + ComponentDto project2 = newPrivateProjectDto(organizationDto); + IssueDoc project2Issue1 = newDoc(project2).setFuncCreationDate(addDays(now, -15)); + IssueDoc project2Issue2 = newDoc(project2).setFuncCreationDate(addDays(now, -30)); + indexIssues(project1Issue1, project1Issue2, project2Issue1, project2Issue2); + + // Search for issues of project 1 having less than 15 days + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfterByProjectUuids(ImmutableMap.of(project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -15), true))), + project1Issue1.key()); + + // Search for issues of project 1 having less than 14 days and project 2 having less then 25 days + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfterByProjectUuids(ImmutableMap.of( + project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -14), true), + project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -25), true))), + project1Issue1.key(), project2Issue1.key()); + + // Search for issues of project 1 having less than 30 days + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfterByProjectUuids(ImmutableMap.of( + project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -30), true))), + project1Issue1.key(), project1Issue2.key()); + + // Search for issues of project 1 and project 2 having less than 5 days + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfterByProjectUuids(ImmutableMap.of( + project1.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true), + project2.uuid(), new IssueQuery.PeriodStart(addDays(now, -5), true)))); + } + + @Test + public void filter_by_severities() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setSeverity(Severity.INFO), + newDoc("I2", file).setSeverity(Severity.MAJOR)); + + assertThatSearchReturnsOnly(IssueQuery.builder().severities(asList(Severity.INFO, Severity.MAJOR)), "I1", "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().severities(singletonList(Severity.INFO)), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().severities(singletonList(Severity.BLOCKER))); + } + + @Test + public void facets_on_severities() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setSeverity(Severity.INFO), + newDoc("I2", file).setSeverity(Severity.INFO), + newDoc("I3", file).setSeverity(Severity.MAJOR)); + + assertThatFacetHasOnly(IssueQuery.builder(), "severities", entry("INFO", 2L), entry("MAJOR", 1L)); + } + + @Test + public void filter_by_statuses() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setStatus(Issue.STATUS_CLOSED), + newDoc("I2", file).setStatus(Issue.STATUS_OPEN)); + + assertThatSearchReturnsOnly(IssueQuery.builder().statuses(asList(Issue.STATUS_CLOSED, Issue.STATUS_OPEN)), "I1", "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().statuses(singletonList(Issue.STATUS_CLOSED)), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().statuses(singletonList(Issue.STATUS_CONFIRMED))); + } + + @Test + public void facets_on_statuses() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setStatus(Issue.STATUS_CLOSED), + newDoc("I2", file).setStatus(Issue.STATUS_CLOSED), + newDoc("I3", file).setStatus(Issue.STATUS_OPEN)); + + assertThatFacetHasOnly(IssueQuery.builder(), "statuses", entry("CLOSED", 2L), entry("OPEN", 1L)); + } + + @Test + public void filter_by_resolutions() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), + newDoc("I2", file).setResolution(Issue.RESOLUTION_FIXED)); + + assertThatSearchReturnsOnly(IssueQuery.builder().resolutions(asList(Issue.RESOLUTION_FALSE_POSITIVE, Issue.RESOLUTION_FIXED)), "I1", "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().resolutions(singletonList(Issue.RESOLUTION_FALSE_POSITIVE)), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().resolutions(singletonList(Issue.RESOLUTION_REMOVED))); + } + + @Test + public void facets_on_resolutions() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), + newDoc("I2", file).setResolution(Issue.RESOLUTION_FALSE_POSITIVE), + newDoc("I3", file).setResolution(Issue.RESOLUTION_FIXED)); + + assertThatFacetHasOnly(IssueQuery.builder(), "resolutions", entry("FALSE-POSITIVE", 2L), entry("FIXED", 1L)); + } + + @Test + public void filter_by_resolved() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED), + newDoc("I2", file).setStatus(Issue.STATUS_OPEN).setResolution(null), + newDoc("I3", file).setStatus(Issue.STATUS_OPEN).setResolution(null)); + + assertThatSearchReturnsOnly(IssueQuery.builder().resolved(true), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().resolved(false), "I2", "I3"); + assertThatSearchReturnsOnly(IssueQuery.builder().resolved(null), "I1", "I2", "I3"); + } + + @Test + public void filter_by_rules() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + RuleDefinitionDto ruleDefinitionDto = newRule(); + db.rules().insert(ruleDefinitionDto); + + indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId())); + + assertThatSearchReturnsOnly(IssueQuery.builder().rules(singletonList(ruleDefinitionDto)), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().rules(singletonList(new RuleDefinitionDto().setId(-1)))); + } + + @Test + public void filter_by_languages() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + RuleDefinitionDto ruleDefinitionDto = newRule(); + db.rules().insert(ruleDefinitionDto); + + indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId()).setLanguage("xoo")); + + assertThatSearchReturnsOnly(IssueQuery.builder().languages(singletonList("xoo")), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().languages(singletonList("unknown"))); + } + + @Test + public void facets_on_languages() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + RuleDefinitionDto ruleDefinitionDto = newRule(); + db.rules().insert(ruleDefinitionDto); + + indexIssues(newDoc("I1", file).setRuleId(ruleDefinitionDto.getId()).setLanguage("xoo")); + + assertThatFacetHasOnly(IssueQuery.builder(), "languages", entry("xoo", 1L)); + } + + @Test + public void filter_by_assignees() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAssigneeUuid("steph-uuid"), + newDoc("I2", file).setAssigneeUuid("marcel-uuid"), + newDoc("I3", file).setAssigneeUuid(null)); + + assertThatSearchReturnsOnly(IssueQuery.builder().assigneeUuids(singletonList("steph-uuid")), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().assigneeUuids(asList("steph-uuid", "marcel-uuid")), "I1", "I2"); + assertThatSearchReturnsEmpty(IssueQuery.builder().assigneeUuids(singletonList("unknown"))); + } + + @Test + public void facets_on_assignees() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAssigneeUuid("steph-uuid"), + newDoc("I2", file).setAssigneeUuid("marcel-uuid"), + newDoc("I3", file).setAssigneeUuid("marcel-uuid"), + newDoc("I4", file).setAssigneeUuid(null)); + + assertThatFacetHasOnly(IssueQuery.builder(), "assignees", entry("steph-uuid", 1L), entry("marcel-uuid", 2L), entry("", 1L)); + } + + @Test + public void facets_on_assignees_supports_dashes() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAssigneeUuid("j-b-uuid"), + newDoc("I2", file).setAssigneeUuid("marcel-uuid"), + newDoc("I3", file).setAssigneeUuid("marcel-uuid"), + newDoc("I4", file).setAssigneeUuid(null)); + + assertThatFacetHasOnly(IssueQuery.builder().assigneeUuids(singletonList("j-b")), + "assignees", entry("j-b-uuid", 1L), entry("marcel-uuid", 2L), entry("", 1L)); + } + + @Test + public void filter_by_assigned() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAssigneeUuid("steph-uuid"), + newDoc("I2", file).setAssigneeUuid(null), + newDoc("I3", file).setAssigneeUuid(null)); + + assertThatSearchReturnsOnly(IssueQuery.builder().assigned(true), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().assigned(false), "I2", "I3"); + assertThatSearchReturnsOnly(IssueQuery.builder().assigned(null), "I1", "I2", "I3"); + } + + @Test + public void filter_by_authors() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAuthorLogin("steph"), + newDoc("I2", file).setAuthorLogin("marcel"), + newDoc("I3", file).setAssigneeUuid(null)); + + assertThatSearchReturnsOnly(IssueQuery.builder().authors(singletonList("steph")), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().authors(asList("steph", "marcel")), "I1", "I2"); + assertThatSearchReturnsEmpty(IssueQuery.builder().authors(singletonList("unknown"))); + } + + @Test + public void facets_on_authors() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setAuthorLogin("steph"), + newDoc("I2", file).setAuthorLogin("marcel"), + newDoc("I3", file).setAuthorLogin("marcel"), + newDoc("I4", file).setAuthorLogin(null)); + + assertThatFacetHasOnly(IssueQuery.builder(), "authors", entry("steph", 1L), entry("marcel", 2L)); + } + + @Test + public void filter_by_created_after() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), + newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); + + assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-19")), "I1", "I2"); + // Lower bound is included + assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-20")), "I1", "I2"); + assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-21")), "I2"); + assertThatSearchReturnsEmpty(IssueQuery.builder().createdAfter(parseDate("2014-09-25"))); + } + + @Test + public void filter_by_created_before() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), + newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); + + assertThatSearchReturnsEmpty(IssueQuery.builder().createdBefore(parseDate("2014-09-19"))); + // Upper bound is excluded + assertThatSearchReturnsEmpty(IssueQuery.builder().createdBefore(parseDate("2014-09-20"))); + assertThatSearchReturnsOnly(IssueQuery.builder().createdBefore(parseDate("2014-09-21")), "I1"); + assertThatSearchReturnsOnly(IssueQuery.builder().createdBefore(parseDate("2014-09-25")), "I1", "I2"); + } + + @Test + public void filter_by_created_after_and_before() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20")), + newDoc("I2", file).setFuncCreationDate(parseDate("2014-09-23"))); + + // 19 < createdAt < 25 + assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDate("2014-09-19")).createdBefore(parseDate("2014-09-25")), + "I1", "I2"); + + // 20 < createdAt < 25: excludes first issue + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-25")), "I1", "I2"); + + // 21 < createdAt < 25 + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-25")), "I2"); + + // 21 < createdAt < 24 + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-24")), "I2"); + + // 21 < createdAt < 23: excludes second issue + assertThatSearchReturnsEmpty(IssueQuery.builder() + .createdAfter(parseDate("2014-09-21")).createdBefore(parseDate("2014-09-23"))); + + // 19 < createdAt < 21: only first issue + assertThatSearchReturnsOnly(IssueQuery.builder() + .createdAfter(parseDate("2014-09-19")).createdBefore(parseDate("2014-09-21")), "I1"); + + // 20 < createdAt < 20: exception + expectedException.expect(IllegalArgumentException.class); + underTest.search(IssueQuery.builder() + .createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-20")) + .build(), new SearchOptions()); + } + + @Test + public void filter_by_create_after_and_before_take_into_account_timezone() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-20T00:00:00+0100")), + newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100"))); + + assertThatSearchReturnsOnly(IssueQuery.builder().createdAfter(parseDateTime("2014-09-19T23:00:00+0000")).createdBefore(parseDateTime("2014-09-22T23:00:01+0000")), + "I1", "I2"); + + assertThatSearchReturnsEmpty(IssueQuery.builder().createdAfter(parseDateTime("2014-09-19T23:00:01+0000")).createdBefore(parseDateTime("2014-09-22T23:00:00+0000"))); + } + + @Test + public void filter_by_created_before_must_be_lower_than_after() { + try { + underTest.search(IssueQuery.builder().createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-19")).build(), + new SearchOptions()); + Fail.failBecauseExceptionWasNotThrown(IllegalArgumentException.class); + } catch (IllegalArgumentException exception) { + assertThat(exception.getMessage()).isEqualTo("Start bound cannot be larger or equal to end bound"); + } + } + + @Test + public void fail_if_created_before_equals_created_after() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Start bound cannot be larger or equal to end bound"); + + underTest.search(IssueQuery.builder().createdAfter(parseDate("2014-09-20")).createdBefore(parseDate("2014-09-20")).build(), new SearchOptions()); + } + + @Test + public void filter_by_created_after_must_not_be_in_future() { + try { + underTest.search(IssueQuery.builder().createdAfter(new Date(Long.MAX_VALUE)).build(), new SearchOptions()); + Fail.failBecauseExceptionWasNotThrown(IllegalArgumentException.class); + } catch (IllegalArgumentException exception) { + assertThat(exception.getMessage()).isEqualTo("Start bound cannot be in the future"); + } + } + + @Test + public void filter_by_created_at() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues(newDoc("I1", file).setFuncCreationDate(parseDate("2014-09-20"))); + + assertThatSearchReturnsOnly(IssueQuery.builder().createdAt(parseDate("2014-09-20")), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().createdAt(parseDate("2014-09-21"))); + } + + @Test + public void facet_on_created_at_with_less_than_20_days() { + SearchOptions options = fixtureForCreatedAtFacet(); + + IssueQuery query = IssueQuery.builder() + .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) + .createdBefore(parseDateTime("2014-09-08T00:00:00+0100")) + .checkAuthorization(false) + .build(); + SearchResponse result = underTest.search(query, options); + Map buckets = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(buckets).containsOnly( + entry("2014-08-31", 0L), + entry("2014-09-01", 2L), + entry("2014-09-02", 1L), + entry("2014-09-03", 0L), + entry("2014-09-04", 0L), + entry("2014-09-05", 1L), + entry("2014-09-06", 0L), + entry("2014-09-07", 0L)); + } + + @Test + public void facet_on_created_at_with_less_than_20_weeks() { + SearchOptions options = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) + .createdBefore(parseDateTime("2014-09-21T00:00:00+0100")).build(), + options); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2014-08-25", 0L), + entry("2014-09-01", 4L), + entry("2014-09-08", 0L), + entry("2014-09-15", 1L)); + } + + @Test + public void facet_on_created_at_with_less_than_20_months() { + SearchOptions options = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdAfter(parseDateTime("2014-09-01T00:00:00+0100")) + .createdBefore(parseDateTime("2015-01-19T00:00:00+0100")).build(), + options); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2014-08-01", 0L), + entry("2014-09-01", 5L), + entry("2014-10-01", 0L), + entry("2014-11-01", 0L), + entry("2014-12-01", 0L), + entry("2015-01-01", 1L)); + } + + @Test + public void facet_on_created_at_with_more_than_20_months() { + SearchOptions options = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdAfter(parseDateTime("2011-01-01T00:00:00+0100")) + .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), + options); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2010-01-01", 0L), + entry("2011-01-01", 1L), + entry("2012-01-01", 0L), + entry("2013-01-01", 0L), + entry("2014-01-01", 5L), + entry("2015-01-01", 1L)); + } + + @Test + public void facet_on_created_at_with_one_day() { + SearchOptions options = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdAfter(parseDateTime("2014-09-01T00:00:00-0100")) + .createdBefore(parseDateTime("2014-09-02T00:00:00-0100")).build(), + options); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2014-09-01", 2L)); + } + + @Test + public void facet_on_created_at_with_bounds_outside_of_data() { + SearchOptions options = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdAfter(parseDateTime("2009-01-01T00:00:00+0100")) + .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")) + .build(), options); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2008-01-01", 0L), + entry("2009-01-01", 0L), + entry("2010-01-01", 0L), + entry("2011-01-01", 1L), + entry("2012-01-01", 0L), + entry("2013-01-01", 0L), + entry("2014-01-01", 5L), + entry("2015-01-01", 1L)); + } + + @Test + public void facet_on_created_at_without_start_bound() { + SearchOptions searchOptions = fixtureForCreatedAtFacet(); + + SearchResponse result = underTest.search(IssueQuery.builder() + .createdBefore(parseDateTime("2016-01-01T00:00:00+0100")).build(), + searchOptions); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).containsOnly( + entry("2011-01-01", 1L), + entry("2012-01-01", 0L), + entry("2013-01-01", 0L), + entry("2014-01-01", 5L), + entry("2015-01-01", 1L)); + } + + @Test + public void facet_on_created_at_without_issues() { + SearchOptions searchOptions = new SearchOptions().addFacets("createdAt"); + + SearchResponse result = underTest.search(IssueQuery.builder().build(), searchOptions); + Map createdAt = new Facets(result, system2.getDefaultTimeZone()).get("createdAt"); + assertThat(createdAt).isNull(); + } + + private SearchOptions fixtureForCreatedAtFacet() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + IssueDoc issue0 = newDoc("ISSUE0", file).setFuncCreationDate(parseDateTime("2011-04-25T00:05:13+0000")); + IssueDoc issue1 = newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-01T12:34:56+0100")); + IssueDoc issue2 = newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-01T10:46:00-1200")); + IssueDoc issue3 = newDoc("I3", file).setFuncCreationDate(parseDateTime("2014-09-02T23:34:56+1200")); + IssueDoc issue4 = newDoc("I4", file).setFuncCreationDate(parseDateTime("2014-09-05T12:34:56+0100")); + IssueDoc issue5 = newDoc("I5", file).setFuncCreationDate(parseDateTime("2014-09-20T12:34:56+0100")); + IssueDoc issue6 = newDoc("I6", file).setFuncCreationDate(parseDateTime("2015-01-18T12:34:56+0100")); + + indexIssues(issue0, issue1, issue2, issue3, issue4, issue5, issue6); + + return new SearchOptions().addFacets("createdAt"); + } + + @Test + public void paging() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + for (int i = 0; i < 12; i++) { + indexIssues(newDoc("I" + i, file)); + } + + IssueQuery.Builder query = IssueQuery.builder(); + // There are 12 issues in total, with 10 issues per page, the page 2 should only contain 2 elements + SearchResponse result = underTest.search(query.build(), new SearchOptions().setPage(2, 10)); + assertThat(result.getHits().hits()).hasSize(2); + assertThat(result.getHits().getTotalHits()).isEqualTo(12); + + result = underTest.search(IssueQuery.builder().build(), new SearchOptions().setOffset(0).setLimit(5)); + assertThat(result.getHits().hits()).hasSize(5); + assertThat(result.getHits().getTotalHits()).isEqualTo(12); + + result = underTest.search(IssueQuery.builder().build(), new SearchOptions().setOffset(2).setLimit(0)); + assertThat(result.getHits().hits()).hasSize(10); + assertThat(result.getHits().getTotalHits()).isEqualTo(12); + } + + @Test + public void search_with_max_limit() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + List issues = new ArrayList<>(); + for (int i = 0; i < 500; i++) { + String key = "I" + i; + issues.add(newDoc(key, file)); + } + indexIssues(issues.toArray(new IssueDoc[] {})); + + IssueQuery.Builder query = IssueQuery.builder(); + SearchResponse result = underTest.search(query.build(), new SearchOptions().setLimit(Integer.MAX_VALUE)); + assertThat(result.getHits().hits()).hasSize(SearchOptions.MAX_LIMIT); + } + + @Test + public void sort_by_status() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setStatus(Issue.STATUS_OPEN), + newDoc("I2", file).setStatus(Issue.STATUS_CLOSED), + newDoc("I3", file).setStatus(Issue.STATUS_REOPENED)); + + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(true); + assertThatSearchReturnsOnly(query, "I2", "I1", "I3"); + + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_STATUS).asc(false); + assertThatSearchReturnsOnly(query, "I3", "I1", "I2"); + } + + @Test + public void sort_by_severity() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setSeverity(Severity.BLOCKER), + newDoc("I2", file).setSeverity(Severity.INFO), + newDoc("I3", file).setSeverity(Severity.MINOR), + newDoc("I4", file).setSeverity(Severity.CRITICAL), + newDoc("I5", file).setSeverity(Severity.MAJOR)); + + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(true); + assertThatSearchReturnsOnly(query, "I2", "I3", "I5", "I4", "I1"); + + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_SEVERITY).asc(false); + assertThatSearchReturnsOnly(query, "I1", "I4", "I5", "I3", "I2"); + } + + @Test + public void sort_by_creation_date() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("I2", file).setFuncCreationDate(parseDateTime("2014-09-24T00:00:00+0100"))); + + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(true); + SearchResponse result = underTest.search(query.build(), new SearchOptions()); + assertThatSearchReturnsOnly(query, "I1", "I2"); + + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CREATION_DATE).asc(false); + assertThatSearchReturnsOnly(query, "I2", "I1"); + } + + @Test + public void sort_by_update_date() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncUpdateDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("I2", file).setFuncUpdateDate(parseDateTime("2014-09-24T00:00:00+0100"))); + + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(true); + SearchResponse result = underTest.search(query.build(), new SearchOptions()); + assertThatSearchReturnsOnly(query, "I1", "I2"); + + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_UPDATE_DATE).asc(false); + assertThatSearchReturnsOnly(query, "I2", "I1"); + } + + @Test + public void sort_by_close_date() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + + indexIssues( + newDoc("I1", file).setFuncCloseDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("I2", file).setFuncCloseDate(parseDateTime("2014-09-24T00:00:00+0100")), + newDoc("I3", file).setFuncCloseDate(null)); + + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(true); + SearchResponse result = underTest.search(query.build(), new SearchOptions()); + assertThatSearchReturnsOnly(query, "I3", "I1", "I2"); + + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_CLOSE_DATE).asc(false); + assertThatSearchReturnsOnly(query, "I2", "I1", "I3"); + } + + @Test + public void sort_by_file_and_line() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file1 = newFileDto(project, null, "F1").setPath("src/main/xoo/org/sonar/samples/File.xoo"); + ComponentDto file2 = newFileDto(project, null, "F2").setPath("src/main/xoo/org/sonar/samples/File2.xoo"); + + indexIssues( + // file F1 + newDoc("F1_2", file1).setLine(20), + newDoc("F1_1", file1).setLine(null), + newDoc("F1_3", file1).setLine(25), + + // file F2 + newDoc("F2_1", file2).setLine(9), + newDoc("F2_2", file2).setLine(109), + // two issues on the same line -> sort by key + newDoc("F2_3", file2).setLine(109)); + + // ascending sort -> F1 then F2. Line "0" first. + IssueQuery.Builder query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(true); + assertThatSearchReturnsOnly(query, "F1_1", "F1_2", "F1_3", "F2_1", "F2_2", "F2_3"); + + // descending sort -> F2 then F1 + query = IssueQuery.builder().sort(IssueQuery.SORT_BY_FILE_LINE).asc(false); + assertThatSearchReturnsOnly(query, "F2_3", "F2_2", "F2_1", "F1_3", "F1_2", "F1_1"); + } + + @Test + public void default_sort_is_by_creation_date_then_project_then_file_then_line_then_issue_key() { + OrganizationDto organizationDto = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(organizationDto, "P1"); + ComponentDto file1 = newFileDto(project1, null, "F1").setPath("src/main/xoo/org/sonar/samples/File.xoo"); + ComponentDto file2 = newFileDto(project1, null, "F2").setPath("src/main/xoo/org/sonar/samples/File2.xoo"); + + ComponentDto project2 = newPrivateProjectDto(organizationDto, "P2"); + ComponentDto file3 = newFileDto(project2, null, "F3").setPath("src/main/xoo/org/sonar/samples/File3.xoo"); + + indexIssues( + // file F1 from project P1 + newDoc("F1_1", file1).setLine(20).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("F1_2", file1).setLine(null).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("F1_3", file1).setLine(25).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + + // file F2 from project P1 + newDoc("F2_1", file2).setLine(9).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + newDoc("F2_2", file2).setLine(109).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + // two issues on the same line -> sort by key + newDoc("F2_3", file2).setLine(109).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100")), + + // file F3 from project P2 + newDoc("F3_1", file3).setLine(20).setFuncCreationDate(parseDateTime("2014-09-24T00:00:00+0100")), + newDoc("F3_2", file3).setLine(20).setFuncCreationDate(parseDateTime("2014-09-23T00:00:00+0100"))); + + assertThatSearchReturnsOnly(IssueQuery.builder(), "F3_1", "F1_2", "F1_1", "F1_3", "F2_1", "F2_2", "F2_3", "F3_2"); + } + + @Test + public void authorized_issues_on_groups() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(org); + ComponentDto project2 = newPrivateProjectDto(org); + ComponentDto project3 = newPrivateProjectDto(org); + ComponentDto file1 = newFileDto(project1, null); + ComponentDto file2 = newFileDto(project2, null); + ComponentDto file3 = newFileDto(project3, null); + GroupDto group1 = newGroupDto(); + GroupDto group2 = newGroupDto(); + + // project1 can be seen by group1 + indexIssue(newDoc("I1", file1)); + authorizationIndexerTester.allowOnlyGroup(project1, group1); + // project2 can be seen by group2 + indexIssue(newDoc("I2", file2)); + authorizationIndexerTester.allowOnlyGroup(project2, group2); + // project3 can be seen by nobody + indexIssue(newDoc("I3", file3)); + + userSessionRule.logIn().setGroups(group1); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); + + userSessionRule.logIn().setGroups(group2); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I2"); + + userSessionRule.logIn().setGroups(group1, group2); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I1", "I2"); + + GroupDto otherGroup = newGroupDto(); + userSessionRule.logIn().setGroups(otherGroup); + assertThatSearchReturnsEmpty(IssueQuery.builder()); + + userSessionRule.logIn().setGroups(group1, group2); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(singletonList(project3.uuid()))); + } + + @Test + public void authorized_issues_on_user() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project1 = newPrivateProjectDto(org); + ComponentDto project2 = newPrivateProjectDto(org); + ComponentDto project3 = newPrivateProjectDto(org); + ComponentDto file1 = newFileDto(project1, null); + ComponentDto file2 = newFileDto(project2, null); + ComponentDto file3 = newFileDto(project3, null); + UserDto user1 = newUserDto(); + UserDto user2 = newUserDto(); + + // project1 can be seen by john, project2 by max, project3 cannot be seen by anyone + indexIssue(newDoc("I1", file1)); + authorizationIndexerTester.allowOnlyUser(project1, user1); + indexIssue(newDoc("I2", file2)); + authorizationIndexerTester.allowOnlyUser(project2, user2); + indexIssue(newDoc("I3", file3)); + + userSessionRule.logIn(user1); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); + assertThatSearchReturnsEmpty(IssueQuery.builder().projectUuids(asList(project3.getDbKey()))); + + userSessionRule.logIn(user2); + assertThatSearchReturnsOnly(IssueQuery.builder(), "I2"); + + // another user + userSessionRule.logIn(newUserDto()); + assertThatSearchReturnsEmpty(IssueQuery.builder()); + } + + @Test + public void root_user_is_authorized_to_access_all_issues() { + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + indexIssue(newDoc("I1", project)); + userSessionRule.logIn().setRoot(); + + assertThatSearchReturnsOnly(IssueQuery.builder(), "I1"); + } + + @Test + public void list_tags() { + RuleDefinitionDto r1 = db.rules().insert(); + RuleDefinitionDto r2 = db.rules().insert(); + ruleIndexer.commitAndIndex(db.getSession(), asList(r1.getId(), r2.getId())); + + OrganizationDto org = db.organizations().insert(); + OrganizationDto anotherOrg = db.organizations().insert(); + ComponentDto project = newPrivateProjectDto(newOrganizationDto()); + ComponentDto file = newFileDto(project, null); + indexIssues( + newDoc("I42", file).setOrganizationUuid(anotherOrg.getUuid()).setRuleId(r1.getId()).setTags(of("another")), + newDoc("I1", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention", "java8", "bug")), + newDoc("I2", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention", "bug")), + newDoc("I3", file).setOrganizationUuid(org.getUuid()).setRuleId(r2.getId()), + newDoc("I4", file).setOrganizationUuid(org.getUuid()).setRuleId(r1.getId()).setTags(of("convention"))); + + assertThat(underTest.listTags(org, null, 100)).containsOnly("convention", "java8", "bug"); + assertThat(underTest.listTags(org, null, 2)).containsOnly("bug", "convention"); + assertThat(underTest.listTags(org, "vent", 100)).containsOnly("convention"); + assertThat(underTest.listTags(org, null, 1)).containsOnly("bug"); + assertThat(underTest.listTags(org, null, 100)).containsOnly("convention", "java8", "bug"); + assertThat(underTest.listTags(org, "invalidRegexp[", 100)).isEmpty(); + assertThat(underTest.listTags(null, null, 100)).containsExactlyInAnyOrder("another", "convention", "java8", "bug"); + } + + @Test + public void fail_to_list_tags_when_size_greater_than_500() { + OrganizationDto organization = db.organizations().insert(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Page size must be lower than or equals to 500"); + + underTest.listTags(organization, null, 501); + } + + @Test + public void test_listAuthors() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("issue1", project).setAuthorLogin("luke.skywalker"), + newDoc("issue2", project).setAuthorLogin("luke@skywalker.name"), + newDoc("issue3", project).setAuthorLogin(null), + newDoc("issue4", project).setAuthorLogin("anakin@skywalker.name")); + IssueQuery query = IssueQuery.builder() + .checkAuthorization(false) + .build(); + + assertThat(underTest.listAuthors(query, null, 5)).containsExactly("anakin@skywalker.name", "luke.skywalker", "luke@skywalker.name"); + assertThat(underTest.listAuthors(query, null, 2)).containsExactly("anakin@skywalker.name", "luke.skywalker"); + assertThat(underTest.listAuthors(query, "uke", 5)).containsExactly("luke.skywalker", "luke@skywalker.name"); + assertThat(underTest.listAuthors(query, null, 1)).containsExactly("anakin@skywalker.name"); + assertThat(underTest.listAuthors(query, null, Integer.MAX_VALUE)).containsExactly("anakin@skywalker.name", "luke.skywalker", "luke@skywalker.name"); + } + + @Test + public void listAuthors_escapes_regexp_special_characters() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("issue1", project).setAuthorLogin("name++")); + IssueQuery query = IssueQuery.builder() + .checkAuthorization(false) + .build(); + + assertThat(underTest.listAuthors(query, "invalidRegexp[", 5)).isEmpty(); + assertThat(underTest.listAuthors(query, "nam+", 5)).isEmpty(); + assertThat(underTest.listAuthors(query, "name+", 5)).containsExactly("name++"); + assertThat(underTest.listAuthors(query, ".*", 5)).isEmpty(); + } + + @Test + public void filter_by_organization() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto projectInOrg1 = newPrivateProjectDto(org1); + OrganizationDto org2 = newOrganizationDto(); + ComponentDto projectInOrg2 = newPrivateProjectDto(org2); + + indexIssues(newDoc("issueInOrg1", projectInOrg1), newDoc("issue1InOrg2", projectInOrg2), newDoc("issue2InOrg2", projectInOrg2)); + + verifyOrganizationFilter(org1.getUuid(), "issueInOrg1"); + verifyOrganizationFilter(org2.getUuid(), "issue1InOrg2", "issue2InOrg2"); + verifyOrganizationFilter("does_not_exist"); + } + + @Test + public void filter_by_organization_and_project() { + OrganizationDto org1 = newOrganizationDto(); + ComponentDto projectInOrg1 = newPrivateProjectDto(org1); + OrganizationDto org2 = newOrganizationDto(); + ComponentDto projectInOrg2 = newPrivateProjectDto(org2); + + indexIssues(newDoc("issueInOrg1", projectInOrg1), newDoc("issue1InOrg2", projectInOrg2), newDoc("issue2InOrg2", projectInOrg2)); + + // no conflict + IssueQuery.Builder query = IssueQuery.builder().organizationUuid(org1.getUuid()).projectUuids(singletonList(projectInOrg1.uuid())); + assertThatSearchReturnsOnly(query, "issueInOrg1"); + + // conflict + query = IssueQuery.builder().organizationUuid(org1.getUuid()).projectUuids(singletonList(projectInOrg2.uuid())); + assertThatSearchReturnsEmpty(query); + } + + @Test + public void countTags() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("issue1", project).setTags(ImmutableSet.of("convention", "java8", "bug")), + newDoc("issue2", project).setTags(ImmutableSet.of("convention", "bug")), + newDoc("issue3", project).setTags(emptyList()), + newDoc("issue4", project).setTags(ImmutableSet.of("convention", "java8", "bug")).setResolution(Issue.RESOLUTION_FIXED), + newDoc("issue5", project).setTags(ImmutableSet.of("convention"))); + + assertThat(underTest.countTags(projectQuery(project.uuid()), 5)).containsOnly(entry("convention", 3L), entry("bug", 2L), entry("java8", 1L)); + assertThat(underTest.countTags(projectQuery(project.uuid()), 2)).contains(entry("convention", 3L), entry("bug", 2L)).doesNotContainEntry("java8", 1L); + assertThat(underTest.countTags(projectQuery("other"), 10)).isEmpty(); + } + + @Test + public void searchBranchStatistics() { + ComponentDto project = db.components().insertMainBranch(); + ComponentDto branch1 = db.components().insertProjectBranch(project); + ComponentDto branch2 = db.components().insertProjectBranch(project); + ComponentDto branch3 = db.components().insertProjectBranch(project); + ComponentDto fileOnBranch3 = db.components().insertComponent(newFileDto(branch3)); + indexIssues(newDoc(project), + newDoc(branch1).setType(BUG).setResolution(null), newDoc(branch1).setType(VULNERABILITY).setResolution(null), newDoc(branch1).setType(CODE_SMELL).setResolution(null), + newDoc(branch1).setType(CODE_SMELL).setResolution(RESOLUTION_FIXED), + newDoc(branch3).setType(CODE_SMELL).setResolution(null), newDoc(branch3).setType(CODE_SMELL).setResolution(null), + newDoc(fileOnBranch3).setType(CODE_SMELL).setResolution(null), newDoc(fileOnBranch3).setType(CODE_SMELL).setResolution(RESOLUTION_FIXED)); + + List branchStatistics = underTest.searchBranchStatistics(project.uuid(), asList(branch1.uuid(), branch2.uuid(), branch3.uuid())); + + assertThat(branchStatistics).extracting(BranchStatistics::getBranchUuid, BranchStatistics::getBugs, BranchStatistics::getVulnerabilities, BranchStatistics::getCodeSmells) + .containsExactlyInAnyOrder( + tuple(branch1.uuid(), 1L, 1L, 1L), + tuple(branch3.uuid(), 0L, 0L, 3L)); + } + + @Test + public void searchBranchStatistics_on_many_branches() { + ComponentDto project = db.components().insertMainBranch(); + List branchUuids = new ArrayList<>(); + List expectedResult = new ArrayList<>(); + IntStream.range(0, 15).forEach(i -> { + ComponentDto branch = db.components().insertProjectBranch(project); + addIssues(branch, 1 + i, 2 + i, 3 + i); + expectedResult.add(tuple(branch.uuid(), 1L + i, 2L + i, 3L + i)); + branchUuids.add(branch.uuid()); + }); + + List branchStatistics = underTest.searchBranchStatistics(project.uuid(), branchUuids); + + assertThat(branchStatistics) + .extracting(BranchStatistics::getBranchUuid, BranchStatistics::getBugs, BranchStatistics::getVulnerabilities, BranchStatistics::getCodeSmells) + .hasSize(15) + .containsAll(expectedResult); + } + + @Test + public void searchBranchStatistics_on_empty_list() { + ComponentDto project = db.components().insertMainBranch(); + + assertThat(underTest.searchBranchStatistics(project.uuid(), emptyList())).isEmpty(); + assertThat(underTest.searchBranchStatistics(project.uuid(), singletonList("unknown"))).isEmpty(); + } + + @Test + public void test_getOwaspTop10Report_dont_count_vulnerabilities_from_other_projects() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + ComponentDto another = newPrivateProjectDto(org); + indexIssues( + newDoc("anotherProject", another).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.CRITICAL), + newDoc("openvul1", project).setOwaspTop10(singletonList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating) + .contains( + tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */)); + + } + + @Test + public void test_getOwaspTop10Report_dont_count_closed_vulnerabilities() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("openvul1", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR), + newDoc("notopenvul", project).setOwaspTop10(asList("a1")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED).setResolution(Issue.RESOLUTION_FIXED) + .setSeverity(Severity.BLOCKER)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating) + .contains( + tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */)); + } + + @Test + public void test_getOwaspTop10Report_dont_count_old_vulnerabilities() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + // Previous vulnerabilities in projects that are not reanalyzed will have no owasp nor cwe attributes (not even 'unknown') + newDoc("openvulNotReindexed", project).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN).setSeverity(Severity.MAJOR)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating) + .containsOnly( + tuple(0L, OptionalInt.empty())); + } + + @Test + public void test_getOwaspTop10Report_dont_count_hotspots_from_other_projects() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + ComponentDto another = newPrivateProjectDto(org); + indexIssues( + newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN), + newDoc("anotherProject", another).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots) + .contains( + tuple("a1", 1L /* openhotspot1 */)); + } + + @Test + public void test_getOwaspTop10Report_dont_count_closed_hotspots() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("openhotspot1", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN), + newDoc("closedHotspot", project).setOwaspTop10(asList("a1")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_CLOSED) + .setResolution(Issue.RESOLUTION_FIXED)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, false); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getOpenSecurityHotspots) + .contains( + tuple("a1", 1L /* openhotspot1 */)); + } + + @Test + public void test_getOwaspTop10Report_aggregation_no_cwe() { + List owaspTop10Report = indexIssuesAndAssertOwaspReport(false); + + assertThat(owaspTop10Report).allMatch(category -> category.getChildren().isEmpty()); + } + + @Test + public void test_getOwaspTop10Report_aggregation_with_cwe() { + List owaspTop10Report = indexIssuesAndAssertOwaspReport(true); + + Map> cweByOwasp = owaspTop10Report.stream() + .collect(Collectors.toMap(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getChildren)); + + assertThat(cweByOwasp.get("a1")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, + SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) + .containsExactlyInAnyOrder( + tuple("123", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), + tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), + tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); + assertThat(cweByOwasp.get("a3")).extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, + SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) + .containsExactlyInAnyOrder( + tuple("123", 2L /* openvul1, openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 0L, 0L), + tuple("456", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 0L, 1L /* toReviewHotspot */, 0L), + tuple("unknown", 0L, OptionalInt.empty(), 1L /* openhotspot1 */, 0L, 0L)); + } + + private List indexIssuesAndAssertOwaspReport(boolean includeCwe) { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + ComponentDto another = newPrivateProjectDto(org); + indexIssues( + newDoc("openvul1", project).setOwaspTop10(asList("a1", "a3")).setCwe(asList("123", "456")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.MAJOR), + newDoc("openvul2", project).setOwaspTop10(asList("a3", "a6")).setCwe(asList("123")).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("notowaspvul", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.CRITICAL), + newDoc("openhotspot1", project).setOwaspTop10(asList("a1", "a3")).setCwe(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_OPEN), + newDoc("openhotspot2", project).setOwaspTop10(asList("a3", "a6")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REOPENED), + newDoc("toReviewHotspot", project).setOwaspTop10(asList("a5", "a3")).setCwe(asList("456")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_FIXED), + newDoc("WFHotspot", project).setOwaspTop10(asList("a3", "a8")).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_WONT_FIX), + newDoc("notowasphotspot", project).setOwaspTop10(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); + + List owaspTop10Report = underTest.getOwaspTop10Report(project.uuid(), false, includeCwe); + assertThat(owaspTop10Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, + SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) + .containsExactlyInAnyOrder( + tuple("a1", 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), + tuple("a2", 0L, OptionalInt.empty(), 0L, 0L, 0L), + tuple("a3", 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */, 1L /* WFHotspot */), + tuple("a4", 0L, OptionalInt.empty(), 0L, 0L, 0L), + tuple("a5", 0L, OptionalInt.empty(), 0L, 1L/* toReviewHotspot */, 0L), + tuple("a6", 1L /* openvul2 */, OptionalInt.of(2) /* MINOR = B */, 1L /* openhotspot2 */, 0L, 0L), + tuple("a7", 0L, OptionalInt.empty(), 0L, 0L, 0L), + tuple("a8", 0L, OptionalInt.empty(), 0L, 0L, 1L /* WFHotspot */), + tuple("a9", 0L, OptionalInt.empty(), 0L, 0L, 0L), + tuple("a10", 0L, OptionalInt.empty(), 0L, 0L, 0L), + tuple("unknown", 1L /* notowaspvul */, OptionalInt.of(4) /* CRITICAL = D */, 1L /* notowasphotspot */, 0L, 0L)); + return owaspTop10Report; + } + + @Test + public void test_getSansTop25Report_aggregation() { + OrganizationDto org = newOrganizationDto(); + ComponentDto project = newPrivateProjectDto(org); + indexIssues( + newDoc("openvul1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.MAJOR), + newDoc("openvul2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("notopenvul", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED) + .setResolution(Issue.RESOLUTION_FIXED) + .setSeverity(Severity.BLOCKER), + newDoc("notsansvul", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.CRITICAL), + newDoc("openhotspot1", project).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_OPEN), + newDoc("openhotspot2", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_REOPENED), + newDoc("toReviewHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_FIXED), + newDoc("WFHotspot", project).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_WONT_FIX), + newDoc("notowasphotspot", project).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); + + List sansTop25Report = underTest.getSansTop25Report(project.uuid(), false, false); + assertThat(sansTop25Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, + SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) + .containsExactlyInAnyOrder( + tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), + tuple(SANS_TOP_25_RISKY_RESOURCE, 2L /* openvul1,openvul2 */, OptionalInt.of(3)/* MAJOR = C */, 2L/* openhotspot1,openhotspot2 */, 1L /* toReviewHotspot */, + 1L /* WFHotspot */), + tuple(SANS_TOP_25_POROUS_DEFENSES, 1L /* openvul2 */, OptionalInt.of(2)/* MINOR = B */, 1L/* openhotspot2 */, 0L, 0L)); + + assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); + } + + @Test + public void test_getSansTop25Report_aggregation_on_portfolio() { + ComponentDto portfolio1 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto portfolio2 = db.components().insertPrivateApplication(db.getDefaultOrganization()); + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + indexIssues( + newDoc("openvul1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.MAJOR), + newDoc("openvul2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_REOPENED) + .setSeverity(Severity.MINOR), + newDoc("notopenvul", project1).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_CLOSED) + .setResolution(Issue.RESOLUTION_FIXED) + .setSeverity(Severity.BLOCKER), + newDoc("notsansvul", project2).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.VULNERABILITY).setStatus(Issue.STATUS_OPEN) + .setSeverity(Severity.CRITICAL), + newDoc("openhotspot1", project1).setSansTop25(asList(SANS_TOP_25_INSECURE_INTERACTION, SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_OPEN), + newDoc("openhotspot2", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE, SANS_TOP_25_POROUS_DEFENSES)).setType(RuleType.SECURITY_HOTSPOT) + .setStatus(Issue.STATUS_REOPENED), + newDoc("toReviewHotspot", project1).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_FIXED), + newDoc("WFHotspot", project2).setSansTop25(asList(SANS_TOP_25_RISKY_RESOURCE)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_RESOLVED) + .setResolution(Issue.RESOLUTION_WONT_FIX), + newDoc("notowasphotspot", project1).setSansTop25(singletonList(UNKNOWN_STANDARD)).setType(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN)); + + indexView(portfolio1.uuid(), singletonList(project1.uuid())); + indexView(portfolio2.uuid(), singletonList(project2.uuid())); + + List sansTop25Report = underTest.getSansTop25Report(portfolio1.uuid(), true, false); + assertThat(sansTop25Report) + .extracting(SecurityStandardCategoryStatistics::getCategory, SecurityStandardCategoryStatistics::getVulnerabilities, + SecurityStandardCategoryStatistics::getVulnerabiliyRating, SecurityStandardCategoryStatistics::getOpenSecurityHotspots, + SecurityStandardCategoryStatistics::getToReviewSecurityHotspots, SecurityStandardCategoryStatistics::getWontFixSecurityHotspots) + .containsExactlyInAnyOrder( + tuple(SANS_TOP_25_INSECURE_INTERACTION, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L /* openhotspot1 */, 0L, 0L), + tuple(SANS_TOP_25_RISKY_RESOURCE, 1L /* openvul1 */, OptionalInt.of(3)/* MAJOR = C */, 1L/* openhotspot1 */, 1L /* toReviewHotspot */, 0L), + tuple(SANS_TOP_25_POROUS_DEFENSES, 0L, OptionalInt.empty(), 0L, 0L, 0L)); + + assertThat(sansTop25Report).allMatch(category -> category.getChildren().isEmpty()); + } + + private void addIssues(ComponentDto component, int bugs, int vulnerabilities, int codeSmelles) { + List issues = new ArrayList<>(); + IntStream.range(0, bugs).forEach(b -> issues.add(newDoc(component).setType(BUG).setResolution(null))); + IntStream.range(0, vulnerabilities).forEach(v -> issues.add(newDoc(component).setType(VULNERABILITY).setResolution(null))); + IntStream.range(0, codeSmelles).forEach(c -> issues.add(newDoc(component).setType(CODE_SMELL).setResolution(null))); + indexIssues(issues.toArray(new IssueDoc[issues.size()])); + } + + private IssueQuery projectQuery(String projectUuid) { + return IssueQuery.builder().projectUuids(singletonList(projectUuid)).resolved(false).build(); + } + + private void verifyOrganizationFilter(String organizationUuid, String... expectedIssueKeys) { + IssueQuery.Builder query = IssueQuery.builder().organizationUuid(organizationUuid); + assertThatSearchReturnsOnly(query, expectedIssueKeys); + } + + private void indexIssues(IssueDoc... issues) { + issueIndexer.index(asList(issues).iterator()); + for (IssueDoc issue : issues) { + IndexPermissions access = new IndexPermissions(issue.projectUuid(), "TRK"); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + } + } + + private void indexIssue(IssueDoc issue) { + issueIndexer.index(Iterators.singletonIterator(issue)); + } + + private void indexView(String viewUuid, List projects) { + viewIndexer.index(new ViewDoc().setUuid(viewUuid).setProjects(projects)); + } + + /** + * Execute the search request and return the document ids of results. + */ + private List searchAndReturnKeys(IssueQuery.Builder query) { + return Arrays.stream(underTest.search(query.build(), new SearchOptions()).getHits().getHits()) + .map(SearchHit::getId) + .collect(Collectors.toList()); + } + + private void assertThatSearchReturnsOnly(IssueQuery.Builder query, String... expectedIssueKeys) { + List keys = searchAndReturnKeys(query); + assertThat(keys).containsExactlyInAnyOrder(expectedIssueKeys); + } + + private void assertThatSearchReturnsEmpty(IssueQuery.Builder query) { + List keys = searchAndReturnKeys(query); + assertThat(keys).isEmpty(); + } + + private void assertThatFacetHasExactly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { + SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); + Facets facets = new Facets(result, system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly(facet); + assertThat(facets.get(facet)).containsExactly(expectedEntries); + } + + private void assertThatFacetHasOnly(IssueQuery.Builder query, String facet, Map.Entry... expectedEntries) { + SearchResponse result = underTest.search(query.build(), new SearchOptions().addFacets(singletonList(facet))); + Facets facets = new Facets(result, system2.getDefaultTimeZone()); + assertThat(facets.getNames()).containsOnly(facet); + assertThat(facets.get(facet)).containsOnly(expectedEntries); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java new file mode 100644 index 00000000000..4051092050d --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryFactoryTest.java @@ -0,0 +1,549 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import java.time.Clock; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.utils.DateUtils; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.SnapshotDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDbTester; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.issue.SearchRequest; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.sonar.api.utils.DateUtils.addDays; +import static org.sonar.db.component.ComponentTesting.newDirectory; +import static org.sonar.db.component.ComponentTesting.newFileDto; +import static org.sonar.db.component.ComponentTesting.newModuleDto; +import static org.sonar.db.component.ComponentTesting.newProjectCopy; +import static org.sonar.db.component.ComponentTesting.newSubView; +import static org.sonar.db.rule.RuleTesting.newRule; + +public class IssueQueryFactoryTest { + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester db = DbTester.create(); + + private RuleDbTester ruleDbTester = new RuleDbTester(db); + + private Clock clock = mock(Clock.class); + private IssueQueryFactory underTest = new IssueQueryFactory(db.getDbClient(), clock, userSession); + + @Test + public void create_from_parameters() { + UserDto user = db.users().insertUser(u -> u.setLogin("joanna")); + OrganizationDto organization = db.organizations().insert(); + ComponentDto project = db.components().insertPrivateProject(organization); + ComponentDto module = db.components().insertComponent(newModuleDto(project)); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + + RuleDefinitionDto rule1 = ruleDbTester.insert(); + RuleDefinitionDto rule2 = ruleDbTester.insert(); + newRule(RuleKey.of("findbugs", "NullReference")); + SearchRequest request = new SearchRequest() + .setIssues(asList("anIssueKey")) + .setSeverities(asList("MAJOR", "MINOR")) + .setStatuses(asList("CLOSED")) + .setResolutions(asList("FALSE-POSITIVE")) + .setResolved(true) + .setProjectKeys(asList(project.getDbKey())) + .setModuleUuids(asList(module.uuid())) + .setDirectories(asList("aDirPath")) + .setFileUuids(asList(file.uuid())) + .setAssigneesUuid(asList(user.getUuid())) + .setLanguages(asList("xoo")) + .setTags(asList("tag1", "tag2")) + .setOrganization(organization.getKey()) + .setAssigned(true) + .setCreatedAfter("2013-04-16T09:08:24+0200") + .setCreatedBefore("2013-04-17T09:08:24+0200") + .setRules(asList(rule1.getKey().toString(), rule2.getKey().toString())) + .setSort("CREATION_DATE") + .setAsc(true); + + IssueQuery query = underTest.create(request); + + assertThat(query.issueKeys()).containsOnly("anIssueKey"); + assertThat(query.severities()).containsOnly("MAJOR", "MINOR"); + assertThat(query.statuses()).containsOnly("CLOSED"); + assertThat(query.resolutions()).containsOnly("FALSE-POSITIVE"); + assertThat(query.resolved()).isTrue(); + assertThat(query.projectUuids()).containsOnly(project.uuid()); + assertThat(query.moduleUuids()).containsOnly(module.uuid()); + assertThat(query.fileUuids()).containsOnly(file.uuid()); + assertThat(query.assignees()).containsOnly(user.getUuid()); + assertThat(query.languages()).containsOnly("xoo"); + assertThat(query.tags()).containsOnly("tag1", "tag2"); + assertThat(query.organizationUuid()).isEqualTo(organization.getUuid()); + assertThat(query.onComponentOnly()).isFalse(); + assertThat(query.assigned()).isTrue(); + assertThat(query.rules()).hasSize(2); + assertThat(query.directories()).containsOnly("aDirPath"); + assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200")); + assertThat(query.createdAfter().inclusive()).isTrue(); + assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDateTime("2013-04-17T09:08:24+0200")); + assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE); + assertThat(query.asc()).isTrue(); + } + + @Test + public void leak_period_start_date_is_exclusive() { + long leakPeriodStart = addDays(new Date(), -14).getTime(); + + ComponentDto project = db.components().insertPublicProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + + SnapshotDto analysis = db.components().insertSnapshot(project, s -> s.setPeriodDate(leakPeriodStart)); + + SearchRequest request = new SearchRequest() + .setComponentUuids(Collections.singletonList(file.uuid())) + .setOnComponentOnly(true) + .setSinceLeakPeriod(true); + + IssueQuery query = underTest.create(request); + + assertThat(query.componentUuids()).containsOnly(file.uuid()); + assertThat(query.createdAfter().date()).isEqualTo(new Date(leakPeriodStart)); + assertThat(query.createdAfter().inclusive()).isFalse(); + + } + + @Test + public void dates_are_inclusive() { + SearchRequest request = new SearchRequest() + .setCreatedAfter("2013-04-16") + .setCreatedBefore("2013-04-17"); + + IssueQuery query = underTest.create(request); + + assertThat(query.createdAfter().date()).isEqualTo(DateUtils.parseDate("2013-04-16")); + assertThat(query.createdAfter().inclusive()).isTrue(); + assertThat(query.createdBefore()).isEqualTo(DateUtils.parseDate("2013-04-18")); + } + + @Test + public void creation_date_support_localdate() { + SearchRequest request = new SearchRequest() + .setCreatedAt("2013-04-16"); + + IssueQuery query = underTest.create(request); + + assertThat(query.createdAt()).isEqualTo(DateUtils.parseDate("2013-04-16")); + } + + @Test + public void creation_date_support_zoneddatetime() { + SearchRequest request = new SearchRequest() + .setCreatedAt("2013-04-16T09:08:24+0200"); + + IssueQuery query = underTest.create(request); + + assertThat(query.createdAt()).isEqualTo(DateUtils.parseDateTime("2013-04-16T09:08:24+0200")); + } + + @Test + public void add_unknown_when_no_component_found() { + SearchRequest request = new SearchRequest() + .setComponentKeys(asList("does_not_exist")); + + IssueQuery query = underTest.create(request); + + assertThat(query.componentUuids()).containsOnly(""); + } + + @Test + public void query_without_any_parameter() { + SearchRequest request = new SearchRequest(); + + IssueQuery query = underTest.create(request); + + assertThat(query.componentUuids()).isEmpty(); + assertThat(query.projectUuids()).isEmpty(); + assertThat(query.moduleUuids()).isEmpty(); + assertThat(query.moduleRootUuids()).isEmpty(); + assertThat(query.directories()).isEmpty(); + assertThat(query.fileUuids()).isEmpty(); + assertThat(query.viewUuids()).isEmpty(); + assertThat(query.organizationUuid()).isNull(); + assertThat(query.branchUuid()).isNull(); + } + + @Test + public void fail_if_components_and_components_uuid_params_are_set_at_the_same_time() { + SearchRequest request = new SearchRequest() + .setComponentKeys(asList("foo")) + .setComponentUuids(asList("bar")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("At most one of the following parameters can be provided: componentKeys, componentUuids, components, componentRoots, componentUuids"); + + underTest.create(request); + } + + @Test + public void fail_if_both_projects_and_projectUuids_params_are_set() { + SearchRequest request = new SearchRequest() + .setProjectKeys(asList("foo")) + .setProjectUuids(asList("bar")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Parameters projects and projectUuids cannot be set simultaneously"); + + underTest.create(request); + } + + @Test + public void fail_if_both_componentRoots_and_componentRootUuids_params_are_set() { + SearchRequest request = new SearchRequest() + .setComponentRoots(asList("foo")) + .setComponentRootUuids(asList("bar")); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("At most one of the following parameters can be provided: componentKeys, componentUuids, components, componentRoots, componentUuids"); + + underTest.create(request); + } + + @Test + public void fail_if_componentRoots_references_components_with_different_qualifier() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + SearchRequest request = new SearchRequest() + .setComponentRoots(asList(project.getDbKey(), file.getDbKey())); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("All components must have the same qualifier, found FIL,TRK"); + + underTest.create(request); + } + + @Test + public void param_componentRootUuids_enables_search_in_view_tree_if_user_has_permission_on_view() { + ComponentDto view = db.components().insertView(); + SearchRequest request = new SearchRequest() + .setComponentRootUuids(asList(view.uuid())); + userSession.registerComponents(view); + + IssueQuery query = underTest.create(request); + + assertThat(query.viewUuids()).containsOnly(view.uuid()); + assertThat(query.onComponentOnly()).isFalse(); + } + + @Test + public void application_search_project_issues() { + ComponentDto project1 = db.components().insertPublicProject(); + ComponentDto project2 = db.components().insertPublicProject(); + ComponentDto application = db.components().insertApplication(db.getDefaultOrganization()); + db.components().insertComponents(newProjectCopy("PC1", project1, application)); + db.components().insertComponents(newProjectCopy("PC2", project2, application)); + userSession.registerComponents(application, project1, project2); + + IssueQuery result = underTest.create(new SearchRequest().setComponentUuids(singletonList(application.uuid()))); + + assertThat(result.viewUuids()).containsExactlyInAnyOrder(application.uuid()); + } + + @Test + public void application_search_project_issues_on_leak() { + Date now = new Date(); + ComponentDto project1 = db.components().insertPublicProject(); + SnapshotDto analysis1 = db.components().insertSnapshot(project1, s -> s.setPeriodDate(addDays(now, -14).getTime())); + ComponentDto project2 = db.components().insertPublicProject(); + SnapshotDto analysis2 = db.components().insertSnapshot(project2, s -> s.setPeriodDate(null)); + ComponentDto project3 = db.components().insertPublicProject(); + ComponentDto application = db.components().insertApplication(db.getDefaultOrganization()); + db.components().insertComponents(newProjectCopy("PC1", project1, application)); + db.components().insertComponents(newProjectCopy("PC2", project2, application)); + db.components().insertComponents(newProjectCopy("PC3", project3, application)); + userSession.registerComponents(application, project1, project2, project3); + + IssueQuery result = underTest.create(new SearchRequest() + .setComponentUuids(singletonList(application.uuid())) + .setSinceLeakPeriod(true)); + + assertThat(result.createdAfterByProjectUuids()).hasSize(1); + assertThat(result.createdAfterByProjectUuids().get(project1.uuid()).date().getTime()).isEqualTo(analysis1.getPeriodDate()); + assertThat(result.createdAfterByProjectUuids().get(project1.uuid()).inclusive()).isFalse(); + assertThat(result.viewUuids()).containsExactlyInAnyOrder(application.uuid()); + } + + @Test + public void return_empty_results_if_not_allowed_to_search_for_subview() { + ComponentDto view = db.components().insertView(); + ComponentDto subView = db.components().insertComponent(newSubView(view)); + SearchRequest request = new SearchRequest() + .setComponentRootUuids(asList(subView.uuid())); + + IssueQuery query = underTest.create(request); + + assertThat(query.viewUuids()).containsOnly(""); + } + + @Test + public void param_componentUuids_enables_search_on_project_tree_by_default() { + ComponentDto project = db.components().insertPrivateProject(); + SearchRequest request = new SearchRequest() + .setComponentUuids(asList(project.uuid())); + + IssueQuery query = underTest.create(request); + assertThat(query.projectUuids()).containsExactly(project.uuid()); + assertThat(query.onComponentOnly()).isFalse(); + } + + @Test + public void onComponentOnly_restricts_search_to_specified_componentKeys() { + ComponentDto project = db.components().insertPrivateProject(); + SearchRequest request = new SearchRequest() + .setComponentKeys(asList(project.getDbKey())) + .setOnComponentOnly(true); + + IssueQuery query = underTest.create(request); + + assertThat(query.projectUuids()).isEmpty(); + assertThat(query.componentUuids()).containsExactly(project.uuid()); + assertThat(query.onComponentOnly()).isTrue(); + } + + @Test + public void should_search_in_tree_with_module_uuid() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto module = db.components().insertComponent(newModuleDto(project)); + SearchRequest request = new SearchRequest() + .setComponentUuids(asList(module.uuid())); + + IssueQuery query = underTest.create(request); + assertThat(query.moduleRootUuids()).containsExactly(module.uuid()); + assertThat(query.onComponentOnly()).isFalse(); + } + + @Test + public void param_componentUuids_enables_search_in_directory_tree() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto dir = db.components().insertComponent(newDirectory(project, "src/main/java/foo")); + SearchRequest request = new SearchRequest() + .setComponentUuids(asList(dir.uuid())); + + IssueQuery query = underTest.create(request); + + assertThat(query.moduleUuids()).containsOnly(dir.moduleUuid()); + assertThat(query.directories()).containsOnly(dir.path()); + assertThat(query.onComponentOnly()).isFalse(); + } + + @Test + public void param_componentUuids_enables_search_by_file() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project)); + SearchRequest request = new SearchRequest() + .setComponentUuids(asList(file.uuid())); + + IssueQuery query = underTest.create(request); + + assertThat(query.fileUuids()).containsExactly(file.uuid()); + } + + @Test + public void param_componentUuids_enables_search_by_test_file() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto file = db.components().insertComponent(newFileDto(project).setQualifier(Qualifiers.UNIT_TEST_FILE)); + SearchRequest request = new SearchRequest() + .setComponentUuids(asList(file.uuid())); + + IssueQuery query = underTest.create(request); + + assertThat(query.fileUuids()).containsExactly(file.uuid()); + } + + @Test + public void search_issue_from_branch() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + + assertThat(underTest.create(new SearchRequest() + .setProjectKeys(singletonList(branch.getKey())) + .setBranch(branch.getBranch()))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(project.uuid()), false); + + assertThat(underTest.create(new SearchRequest() + .setComponentKeys(singletonList(branch.getKey())) + .setBranch(branch.getBranch()))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(project.uuid()), false); + } + + @Test + public void search_file_issue_from_branch() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + ComponentDto file = db.components().insertComponent(newFileDto(branch)); + + assertThat(underTest.create(new SearchRequest() + .setComponentKeys(singletonList(file.getKey())) + .setBranch(branch.getBranch()))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(file.uuid()), false); + + assertThat(underTest.create(new SearchRequest() + .setComponentKeys(singletonList(branch.getKey())) + .setFileUuids(singletonList(file.uuid())) + .setBranch(branch.getBranch()))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(file.uuid()), false); + + assertThat(underTest.create(new SearchRequest() + .setProjectKeys(singletonList(branch.getKey())) + .setFileUuids(singletonList(file.uuid())) + .setBranch(branch.getBranch()))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.fileUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(file.uuid()), false); + } + + @Test + public void search_issue_on_component_only_from_branch() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentDto branch = db.components().insertProjectBranch(project); + ComponentDto file = db.components().insertComponent(newFileDto(branch)); + + assertThat(underTest.create(new SearchRequest() + .setComponentKeys(singletonList(file.getKey())) + .setBranch(branch.getBranch()) + .setOnComponentOnly(true))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.componentUuids()), IssueQuery::isMainBranch) + .containsOnly(branch.uuid(), singletonList(file.uuid()), false); + } + + @Test + public void search_issues_from_main_branch() { + ComponentDto project = db.components().insertMainBranch(); + ComponentDto branch = db.components().insertProjectBranch(project); + + assertThat(underTest.create(new SearchRequest() + .setProjectKeys(singletonList(project.getKey())) + .setBranch("master"))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) + .containsOnly(project.uuid(), singletonList(project.uuid()), true); + assertThat(underTest.create(new SearchRequest() + .setComponentKeys(singletonList(project.getKey())) + .setBranch("master"))) + .extracting(IssueQuery::branchUuid, query -> new ArrayList<>(query.projectUuids()), IssueQuery::isMainBranch) + .containsOnly(project.uuid(), singletonList(project.uuid()), true); + } + + @Test + public void fail_if_created_after_and_created_since_are_both_set() { + SearchRequest request = new SearchRequest() + .setCreatedAfter("2013-07-25T07:35:00+0100") + .setCreatedInLast("palap"); + + try { + underTest.create(request); + fail(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Parameters createdAfter and createdInLast cannot be set simultaneously"); + } + } + + @Test + public void set_created_after_from_created_since() { + Date now = DateUtils.parseDateTime("2013-07-25T07:35:00+0100"); + when(clock.instant()).thenReturn(now.toInstant()); + when(clock.getZone()).thenReturn(ZoneOffset.UTC); + SearchRequest request = new SearchRequest() + .setCreatedInLast("1y2m3w4d"); + assertThat(underTest.create(request).createdAfter().date()).isEqualTo(DateUtils.parseDateTime("2012-04-30T07:35:00+0100")); + assertThat(underTest.create(request).createdAfter().inclusive()).isTrue(); + + } + + @Test + public void fail_if_since_leak_period_and_created_after_set_at_the_same_time() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Parameters 'createdAfter' and 'sinceLeakPeriod' cannot be set simultaneously"); + + underTest.create(new SearchRequest() + .setSinceLeakPeriod(true) + .setCreatedAfter("2013-07-25T07:35:00+0100")); + } + + @Test + public void fail_if_no_component_provided_with_since_leak_period() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("One and only one component must be provided when searching since leak period"); + + underTest.create(new SearchRequest().setSinceLeakPeriod(true)); + } + + @Test + public void fail_if_several_components_provided_with_since_leak_period() { + ComponentDto project1 = db.components().insertPrivateProject(); + ComponentDto project2 = db.components().insertPrivateProject(); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("One and only one component must be provided when searching since leak period"); + + underTest.create(new SearchRequest() + .setSinceLeakPeriod(true) + .setComponentKeys(asList(project1.getKey(), project2.getKey()))); + } + + @Test + public void fail_if_date_is_not_formatted_correctly() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("'unknown-date' cannot be parsed as either a date or date+time"); + + underTest.create(new SearchRequest() + .setCreatedAfter("unknown-date")); + } + + @Test + public void return_empty_results_if_organization_with_specified_key_does_not_exist() { + SearchRequest request = new SearchRequest() + .setOrganization("does_not_exist"); + + IssueQuery query = underTest.create(request); + + assertThat(query.organizationUuid()).isEqualTo(""); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java new file mode 100644 index 00000000000..4314de182f6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/index/IssueQueryTest.java @@ -0,0 +1,190 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.index; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import java.util.Date; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.rule.Severity; +import org.sonar.db.rule.RuleDefinitionDto; +import org.sonar.server.issue.index.IssueQuery.PeriodStart; + +import static com.google.common.collect.Lists.newArrayList; +import static org.apache.commons.lang.math.RandomUtils.nextInt; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +public class IssueQueryTest { + + @Test + public void build_query() { + RuleDefinitionDto rule = new RuleDefinitionDto().setId(nextInt(1000)); + PeriodStart filterDate = new IssueQuery.PeriodStart(new Date(10_000_000_000L), false); + IssueQuery query = IssueQuery.builder() + .issueKeys(newArrayList("ABCDE")) + .severities(newArrayList(Severity.BLOCKER)) + .statuses(Lists.newArrayList(Issue.STATUS_RESOLVED)) + .resolutions(newArrayList(Issue.RESOLUTION_FALSE_POSITIVE)) + .projectUuids(newArrayList("PROJECT")) + .componentUuids(newArrayList("org/struts/Action.java")) + .moduleUuids(newArrayList("org.struts:core")) + .rules(newArrayList(rule)) + .assigneeUuids(newArrayList("gargantua")) + .languages(newArrayList("xoo")) + .tags(newArrayList("tag1", "tag2")) + .types(newArrayList("RELIABILITY", "SECURITY")) + .owaspTop10(newArrayList("a1", "a2")) + .sansTop25(newArrayList("insecure-interaction", "porous-defenses")) + .cwe(newArrayList("12", "125")) + .organizationUuid("orga") + .branchUuid("my_branch") + .createdAfterByProjectUuids(ImmutableMap.of("PROJECT", filterDate)) + .assigned(true) + .createdAfter(new Date()) + .createdBefore(new Date()) + .createdAt(new Date()) + .resolved(true) + .sort(IssueQuery.SORT_BY_CREATION_DATE) + .asc(true) + .build(); + assertThat(query.issueKeys()).containsOnly("ABCDE"); + assertThat(query.severities()).containsOnly(Severity.BLOCKER); + assertThat(query.statuses()).containsOnly(Issue.STATUS_RESOLVED); + assertThat(query.resolutions()).containsOnly(Issue.RESOLUTION_FALSE_POSITIVE); + assertThat(query.projectUuids()).containsOnly("PROJECT"); + assertThat(query.componentUuids()).containsOnly("org/struts/Action.java"); + assertThat(query.moduleUuids()).containsOnly("org.struts:core"); + assertThat(query.assignees()).containsOnly("gargantua"); + assertThat(query.languages()).containsOnly("xoo"); + assertThat(query.tags()).containsOnly("tag1", "tag2"); + assertThat(query.types()).containsOnly("RELIABILITY", "SECURITY"); + assertThat(query.owaspTop10()).containsOnly("a1", "a2"); + assertThat(query.sansTop25()).containsOnly("insecure-interaction", "porous-defenses"); + assertThat(query.cwe()).containsOnly("12", "125"); + assertThat(query.organizationUuid()).isEqualTo("orga"); + assertThat(query.branchUuid()).isEqualTo("my_branch"); + assertThat(query.createdAfterByProjectUuids()).containsOnly(entry("PROJECT", filterDate)); + assertThat(query.assigned()).isTrue(); + assertThat(query.rules()).containsOnly(rule); + assertThat(query.createdAfter()).isNotNull(); + assertThat(query.createdBefore()).isNotNull(); + assertThat(query.createdAt()).isNotNull(); + assertThat(query.resolved()).isTrue(); + assertThat(query.sort()).isEqualTo(IssueQuery.SORT_BY_CREATION_DATE); + assertThat(query.asc()).isTrue(); + } + + @Test + public void build_query_without_dates() { + IssueQuery query = IssueQuery.builder() + .issueKeys(newArrayList("ABCDE")) + .createdAfter(null) + .createdBefore(null) + .createdAt(null) + .build(); + + assertThat(query.issueKeys()).containsOnly("ABCDE"); + assertThat(query.createdAfter()).isNull(); + assertThat(query.createdBefore()).isNull(); + assertThat(query.createdAt()).isNull(); + } + + @Test + public void throw_exception_if_sort_is_not_valid() { + try { + IssueQuery.builder() + .sort("UNKNOWN") + .build(); + } catch (Exception e) { + assertThat(e).isInstanceOf(IllegalArgumentException.class).hasMessage("Bad sort field: UNKNOWN"); + } + } + + @Test + public void collection_params_should_not_be_null_but_empty() { + IssueQuery query = IssueQuery.builder() + .issueKeys(null) + .projectUuids(null) + .componentUuids(null) + .moduleUuids(null) + .statuses(null) + .assigneeUuids(null) + .resolutions(null) + .rules(null) + .severities(null) + .languages(null) + .tags(null) + .types(null) + .owaspTop10(null) + .sansTop25(null) + .cwe(null) + .createdAfterByProjectUuids(null) + .build(); + assertThat(query.issueKeys()).isEmpty(); + assertThat(query.projectUuids()).isEmpty(); + assertThat(query.componentUuids()).isEmpty(); + assertThat(query.moduleUuids()).isEmpty(); + assertThat(query.statuses()).isEmpty(); + assertThat(query.assignees()).isEmpty(); + assertThat(query.resolutions()).isEmpty(); + assertThat(query.rules()).isEmpty(); + assertThat(query.severities()).isEmpty(); + assertThat(query.languages()).isEmpty(); + assertThat(query.tags()).isEmpty(); + assertThat(query.types()).isEmpty(); + assertThat(query.owaspTop10()).isEmpty(); + assertThat(query.sansTop25()).isEmpty(); + assertThat(query.cwe()).isEmpty(); + assertThat(query.createdAfterByProjectUuids()).isEmpty(); + } + + @Test + public void test_default_query() { + IssueQuery query = IssueQuery.builder().build(); + assertThat(query.issueKeys()).isEmpty(); + assertThat(query.projectUuids()).isEmpty(); + assertThat(query.componentUuids()).isEmpty(); + assertThat(query.moduleUuids()).isEmpty(); + assertThat(query.statuses()).isEmpty(); + assertThat(query.assignees()).isEmpty(); + assertThat(query.resolutions()).isEmpty(); + assertThat(query.rules()).isEmpty(); + assertThat(query.severities()).isEmpty(); + assertThat(query.languages()).isEmpty(); + assertThat(query.tags()).isEmpty(); + assertThat(query.types()).isEmpty(); + assertThat(query.organizationUuid()).isNull(); + assertThat(query.branchUuid()).isNull(); + assertThat(query.assigned()).isNull(); + assertThat(query.createdAfter()).isNull(); + assertThat(query.createdBefore()).isNull(); + assertThat(query.resolved()).isNull(); + assertThat(query.sort()).isNull(); + assertThat(query.createdAfterByProjectUuids()).isEmpty(); + } + + @Test + public void should_accept_null_sort() { + IssueQuery query = IssueQuery.builder().sort(null).build(); + assertThat(query.sort()).isNull(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java index c26a64ce11a..0cb1ee4df4a 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/AuthorsActionTest.java @@ -28,7 +28,7 @@ import org.sonar.server.es.EsTester; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; @@ -47,7 +47,7 @@ public class AuthorsActionTest { public UserSessionRule userSession = UserSessionRule.standalone(); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient())); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); private WsActionTester ws = new WsActionTester(new AuthorsAction(issueIndex)); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java index a18696cfd0a..1438cffe5a6 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/ComponentTagsActionTest.java @@ -26,8 +26,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.sonar.api.server.ws.WebService.Action; import org.sonar.api.server.ws.WebService.Param; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java index 2a601ec3767..3f88cfabadd 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java @@ -39,15 +39,15 @@ import org.sonar.db.organization.OrganizationDto; import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.server.es.EsTester; import org.sonar.server.issue.IssueFieldsSetter; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.TransitionService; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.view.index.ViewIndexer; import org.sonar.server.ws.WsActionTester; @@ -94,7 +94,7 @@ public class SearchActionComponentsTest { public EsTester es = EsTester.create(); private DbClient dbClient = db.getDbClient(); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); private ViewIndexer viewIndexer = new ViewIndexer(dbClient, es.client()); private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSession); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java index 8a55f127529..dc5b66f1721 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java @@ -56,16 +56,16 @@ import org.sonar.server.es.EsTester; import org.sonar.server.es.SearchOptions; import org.sonar.server.es.StartupIndexer; import org.sonar.server.issue.IssueFieldsSetter; -import org.sonar.server.issue.IssueQuery; -import org.sonar.server.issue.IssueQueryFactory; +import org.sonar.server.issue.index.IssueQuery; +import org.sonar.server.issue.index.IssueQueryFactory; import org.sonar.server.issue.TransitionService; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; import org.sonar.server.issue.workflow.FunctionExecutor; import org.sonar.server.issue.workflow.IssueWorkflow; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexer; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; @@ -109,7 +109,7 @@ public class SearchActionTest { private DbClient dbClient = db.getDbClient(); private DbSession session = db.getSession(); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSessionRule); private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java index 8a4809ebda5..100910a2b93 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/TagsActionTest.java @@ -35,9 +35,9 @@ import org.sonar.server.es.EsTester; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; +import org.sonar.server.permission.index.IndexPermissions; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.rule.index.RuleIndex; import org.sonar.server.rule.index.RuleIndexer; import org.sonar.server.tester.UserSessionRule; @@ -62,7 +62,7 @@ public class TagsActionTest { private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbTester.getDbClient(), new IssueIteratorFactory(dbTester.getDbClient())); private RuleIndexer ruleIndexer = new RuleIndexer(es.client(), dbTester.getDbClient()); private PermissionIndexerTester permissionIndexerTester = new PermissionIndexerTester(es, issueIndexer); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSession, new WebAuthorizationTypeSupport(userSession)); private RuleIndex ruleIndex = new RuleIndex(es.client(), System2.INSTANCE); private WsActionTester ws = new WsActionTester(new TagsAction(issueIndex, ruleIndex, dbTester.getDbClient())); @@ -254,7 +254,7 @@ public class TagsActionTest { } private void grantAccess(IssueDto issue) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(issue.getProjectUuid(), "TRK"); + IndexPermissions access = new IndexPermissions(issue.getProjectUuid(), "TRK"); access.addUserId(userSession.getUserId()); permissionIndexerTester.allow(access); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java new file mode 100644 index 00000000000..670ffb15f4e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTest.java @@ -0,0 +1,1477 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; +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; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchIdResult; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static com.google.common.collect.Lists.newArrayList; +import static com.google.common.collect.Sets.newHashSet; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.COVERAGE_KEY; +import static org.sonar.api.measures.Metric.Level.ERROR; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.db.user.GroupTesting.newGroupDto; +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.INDEX_TYPE_PROJECT_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator; + +@RunWith(DataProviderRunner.class) +public class ProjectMeasuresIndexTest { + + private static final String MAINTAINABILITY_RATING = "sqale_rating"; + private static final String NEW_MAINTAINABILITY_RATING_KEY = "new_maintainability_rating"; + private static final String RELIABILITY_RATING = "reliability_rating"; + private static final String NEW_RELIABILITY_RATING = "new_reliability_rating"; + private static final String SECURITY_RATING = "security_rating"; + private static final String NEW_SECURITY_RATING = "new_security_rating"; + private static final String COVERAGE = "coverage"; + private static final String NEW_COVERAGE = "new_coverage"; + private static final String DUPLICATION = "duplicated_lines_density"; + private static final String NEW_DUPLICATION = "new_duplicated_lines_density"; + private static final String NCLOC = "ncloc"; + private static final String NEW_LINES = "new_lines"; + private static final String LANGUAGES = "languages"; + + private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); + private static final ComponentDto PROJECT1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-1").setName("Project 1").setDbKey("key-1"); + private static final ComponentDto PROJECT2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-2").setName("Project 2").setDbKey("key-2"); + private static final ComponentDto PROJECT3 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-3").setName("Project 3").setDbKey("key-3"); + private static final UserDto USER1 = newUserDto(); + private static final UserDto USER2 = newUserDto(); + private static final GroupDto GROUP1 = newGroupDto(); + private static final GroupDto GROUP2 = newGroupDto(); + + @Rule + public EsTester es = EsTester.create(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + @DataProvider + public static Object[][] rating_metric_keys() { + return new Object[][]{{MAINTAINABILITY_RATING}, {NEW_MAINTAINABILITY_RATING_KEY}, {RELIABILITY_RATING}, {NEW_RELIABILITY_RATING}, {SECURITY_RATING}, {NEW_SECURITY_RATING}}; + } + + private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); + private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + + @Test + public void return_empty_if_no_projects() { + assertNoResults(new ProjectMeasuresQuery()); + } + + @Test + public void default_sort_is_by_ascending_case_insensitive_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); + + assertResults(new ProjectMeasuresQuery(), apache1, apache2, apachee, windows); + } + + @Test + public void sort_by_insensitive_name() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee"); + ComponentDto apache = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache").setName("Apache"); + index(newDoc(windows), newDoc(apachee), newDoc(apache)); + + assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(true), apache, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("name").setAsc(false), windows, apachee, apache); + } + + @Test + public void sort_by_ncloc() { + index( + newDoc(PROJECT1, NCLOC, 15_000d), + newDoc(PROJECT2, NCLOC, 30_000d), + newDoc(PROJECT3, NCLOC, 1_000d)); + + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), PROJECT3, PROJECT1, PROJECT2); + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), PROJECT2, PROJECT1, PROJECT3); + } + + @Test + public void sort_by_a_metric_then_by_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index( + newDoc(windows, NCLOC, 10_000d), + newDoc(apachee, NCLOC, 5_000d), + newDoc(apache1, NCLOC, 5_000d), + newDoc(apache2, NCLOC, 5_000d)); + + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(true), apache1, apache2, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("ncloc").setAsc(false), windows, apache1, apache2, apachee); + } + + @Test + public void sort_by_quality_gate_status() { + ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); + index( + newDoc(PROJECT1).setQualityGateStatus(OK.name()), + newDoc(PROJECT2).setQualityGateStatus(ERROR.name()), + newDoc(PROJECT3).setQualityGateStatus(WARN.name()), + newDoc(project4).setQualityGateStatus(OK.name())); + + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), PROJECT1, project4, PROJECT3, PROJECT2); + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), PROJECT2, PROJECT3, PROJECT1, project4); + } + + @Test + public void sort_by_quality_gate_status_then_by_name_then_by_key() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index( + newDoc(windows).setQualityGateStatus(WARN.name()), + newDoc(apachee).setQualityGateStatus(OK.name()), + newDoc(apache1).setQualityGateStatus(OK.name()), + newDoc(apache2).setQualityGateStatus(OK.name())); + + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(true), apache1, apache2, apachee, windows); + assertResults(new ProjectMeasuresQuery().setSort("alert_status").setAsc(false), windows, apache1, apache2, apachee); + } + + @Test + public void paginate_results() { + IntStream.rangeClosed(1, 9) + .forEach(i -> index(newDoc(newPrivateProjectDto(ORG, "P" + i)))); + + SearchIdResult result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().setPage(2, 3)); + + assertThat(result.getIds()).containsExactly("P4", "P5", "P6"); + assertThat(result.getTotal()).isEqualTo(9); + } + + @Test + public void filter_with_lower_than() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 80d)); + + assertResults(query, PROJECT1); + } + + @Test + public void filter_with_lower_than_or_equals() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)); + + assertResults(query, PROJECT1, PROJECT2); + } + + @Test + public void filter_with_greater_than() { + index( + newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), + newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 30_000d)); + assertResults(query, PROJECT2, PROJECT3); + + query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 100_000d)); + assertNoResults(query); + } + + @Test + public void filter_with_greater_than_or_equals() { + index( + newDoc(PROJECT1, COVERAGE, 80d, NCLOC, 30_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 30_001d), + newDoc(PROJECT3, COVERAGE, 80d, NCLOC, 30_001d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 30_001d)); + assertResults(query, PROJECT2, PROJECT3); + + query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GTE, 100_000d)); + assertNoResults(query); + } + + @Test + public void filter_with_equals() { + index( + newDoc(PROJECT1, COVERAGE, 79d, NCLOC, 10_000d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_000d), + newDoc(PROJECT3, COVERAGE, 81d, NCLOC, 10_000d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.EQ, 80d)); + + assertResults(query, PROJECT2); + } + + @Test + public void filter_on_no_data_with_several_projects() { + index( + newDoc(PROJECT1, NCLOC, 1d), + newDoc(PROJECT2, DUPLICATION, 80d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertResults(query, PROJECT1); + } + + @Test + public void filter_on_no_data_should_not_return_projects_with_data_and_other_measures() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, DUPLICATION, 80d, NCLOC, 1d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertNoResults(query); + } + + @Test + public void filter_on_no_data_should_not_return_projects_with_data() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, DUPLICATION, 80d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertNoResults(query); + } + + @Test + public void filter_on_no_data_should_return_projects_with_no_data() { + ComponentDto project = ComponentTesting.newPrivateProjectDto(ORG); + index(newDoc(project, NCLOC, 1d)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().addMetricCriterion(MetricCriterion.createNoData(DUPLICATION)); + + assertResults(query, project); + } + + @Test + public void filter_on_several_metrics() { + index( + newDoc(PROJECT1, COVERAGE, 81d, NCLOC, 10_001d), + newDoc(PROJECT2, COVERAGE, 80d, NCLOC, 10_001d), + newDoc(PROJECT3, COVERAGE, 79d, NCLOC, 10_000d)); + + ProjectMeasuresQuery esQuery = new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LTE, 80d)) + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.GT, 10_000d)) + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 11_000d)); + assertResults(esQuery, PROJECT2); + } + + @Test + public void filter_on_quality_gate_status() { + index( + newDoc(PROJECT1).setQualityGateStatus(OK.name()), + newDoc(PROJECT2).setQualityGateStatus(OK.name()), + newDoc(PROJECT3).setQualityGateStatus(WARN.name())); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().setQualityGateStatus(OK); + assertResults(query, PROJECT1, PROJECT2); + } + + @Test + public void filter_on_languages() { + ComponentDto project4 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("Project-4").setName("Project 4").setDbKey("key-4"); + index( + newDoc(PROJECT1).setLanguages(singletonList("java")), + newDoc(PROJECT2).setLanguages(singletonList("xoo")), + newDoc(PROJECT3).setLanguages(singletonList("xoo")), + newDoc(project4).setLanguages(asList("", "java", "xoo"))); + + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java", "xoo")), PROJECT1, PROJECT2, PROJECT3, project4); + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("java")), PROJECT1, project4); + assertResults(new ProjectMeasuresQuery().setLanguages(newHashSet("unknown"))); + } + + @Test + public void filter_on_query_text() { + ComponentDto windows = ComponentTesting.newPrivateProjectDto(ORG).setUuid("windows").setName("Windows").setDbKey("project1"); + ComponentDto apachee = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apachee").setName("apachee").setDbKey("project2"); + ComponentDto apache1 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-1").setName("Apache").setDbKey("project3"); + ComponentDto apache2 = ComponentTesting.newPrivateProjectDto(ORG).setUuid("apache-2").setName("Apache").setDbKey("project4"); + index(newDoc(windows), newDoc(apachee), newDoc(apache1), newDoc(apache2)); + + assertResults(new ProjectMeasuresQuery().setQueryText("windows"), windows); + assertResults(new ProjectMeasuresQuery().setQueryText("project2"), apachee); + assertResults(new ProjectMeasuresQuery().setQueryText("pAch"), apache1, apache2, apachee); + } + + @Test + public void filter_on_ids() { + index( + newDoc(PROJECT1), + newDoc(PROJECT2), + newDoc(PROJECT3)); + + ProjectMeasuresQuery query = new ProjectMeasuresQuery().setProjectUuids(newHashSet(PROJECT1.uuid(), PROJECT3.uuid())); + assertResults(query, PROJECT1, PROJECT3); + } + + @Test + public void filter_on_tags() { + index( + newDoc(PROJECT1).setTags(newArrayList("finance", "platform")), + newDoc(PROJECT2).setTags(newArrayList("marketing", "platform")), + newDoc(PROJECT3).setTags(newArrayList("finance", "language"))); + + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance")), PROJECT1, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "language")), PROJECT1, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("finance", "marketing")), PROJECT1, PROJECT2, PROJECT3); + assertResults(new ProjectMeasuresQuery().setTags(newHashSet("marketing")), PROJECT2); + assertNoResults(new ProjectMeasuresQuery().setTags(newHashSet("tag 42"))); + } + + @Test + public void filter_on_organization() { + OrganizationDto org1 = OrganizationTesting.newOrganizationDto(); + OrganizationDto org2 = OrganizationTesting.newOrganizationDto(); + ComponentDto projectInOrg1 = ComponentTesting.newPrivateProjectDto(org1); + ComponentDto projectInOrg2 = ComponentTesting.newPrivateProjectDto(org2); + index(newDoc(projectInOrg1), newDoc(projectInOrg2)); + + ProjectMeasuresQuery query1 = new ProjectMeasuresQuery().setOrganizationUuid(org1.getUuid()); + assertResults(query1, projectInOrg1); + + ProjectMeasuresQuery query2 = new ProjectMeasuresQuery().setOrganizationUuid(org2.getUuid()); + assertResults(query2, projectInOrg2); + + ProjectMeasuresQuery query3 = new ProjectMeasuresQuery().setOrganizationUuid("another_org"); + assertNoResults(query3); + } + + @Test + public void return_only_projects_authorized_for_user() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForUser(USER2, newDoc(PROJECT3)); + + userSession.logIn(USER1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + } + + @Test + public void return_only_projects_authorized_for_user_groups() { + indexForGroup(GROUP1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForGroup(GROUP2, newDoc(PROJECT3)); + + userSession.logIn().setGroups(GROUP1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2); + } + + @Test + public void return_only_projects_authorized_for_user_and_groups() { + indexForUser(USER1, newDoc(PROJECT1), newDoc(PROJECT2)); + indexForGroup(GROUP1, newDoc(PROJECT3)); + + userSession.logIn(USER1).setGroups(GROUP1); + assertResults(new ProjectMeasuresQuery(), PROJECT1, PROJECT2, PROJECT3); + } + + @Test + public void anonymous_user_can_only_access_projects_authorized_for_anyone() { + index(newDoc(PROJECT1)); + indexForUser(USER1, newDoc(PROJECT2)); + + userSession.anonymous(); + assertResults(new ProjectMeasuresQuery(), PROJECT1); + } + + @Test + public void root_user_can_access_all_projects() { + indexForUser(USER1, newDoc(PROJECT1)); + // connecting with a root but not USER1 + userSession.logIn().setRoot(); + + assertResults(new ProjectMeasuresQuery(), PROJECT1); + } + + @Test + public void does_not_return_facet_when_no_facets_in_options() { + index( + newDoc(PROJECT1, NCLOC, 10d, COVERAGE_KEY, 30d, MAINTAINABILITY_RATING, 3d) + .setQualityGateStatus(OK.name())); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions()).getFacets(); + + assertThat(facets.getAll()).isEmpty(); + } + + @Test + public void facet_ncloc() { + index( + // 3 docs with ncloc<1K + newDoc(NCLOC, 0d), + newDoc(NCLOC, 0d), + newDoc(NCLOC, 999d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d), + newDoc(NCLOC, 9_999d), + // 4 docs with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 10_000d), + newDoc(NCLOC, 10_000d), + newDoc(NCLOC, 11_000d), + newDoc(NCLOC, 99_000d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 100_000d), + newDoc(NCLOC, 499_000d), + // 5 docs with ncloc>= 500K + newDoc(NCLOC, 500_000d), + newDoc(NCLOC, 100_000_000d), + newDoc(NCLOC, 500_000d), + newDoc(NCLOC, 1_000_000d), + newDoc(NCLOC, 100_000_000_000d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); + + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 4L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 5L)); + } + + @Test + public void facet_ncloc_is_sticky() { + index( + // 1 docs with ncloc<1K + newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), + newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), + // 3 docs with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), + newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), + newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), + newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 0d), + // 1 docs with ncloc>= 500K + newDoc(NCLOC, 501_000d, COVERAGE, 81d, DUPLICATION, 20d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(NCLOC, Operator.LT, 10_000d)) + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), + new SearchOptions().addFacets(NCLOC, COVERAGE)).getFacets(); + + // Sticky facet on ncloc does not take into account ncloc filter + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 3L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 0L)); + // But facet on coverage does well take into into filters + assertThat(facets.get(COVERAGE)).containsOnly( + entry("NO_DATA", 0L), + entry("*-30.0", 3L), + entry("30.0-50.0", 0L), + entry("50.0-70.0", 0L), + entry("70.0-80.0", 0L), + entry("80.0-*", 0L)); + } + + @Test + public void facet_ncloc_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // docs with ncloc<1K + newDoc(NCLOC, 0d), + newDoc(NCLOC, 100d), + newDoc(NCLOC, 999d), + // docs with ncloc>=1K and ncloc<10K + newDoc(NCLOC, 1_000d), + newDoc(NCLOC, 9_999d)); + + // User cannot see these projects + indexForUser(USER2, + // doc with ncloc>=10K and ncloc<100K + newDoc(NCLOC, 11_000d), + // doc with ncloc>=100K and ncloc<500K + newDoc(NCLOC, 499_000d), + // doc with ncloc>= 500K + newDoc(NCLOC, 501_000d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NCLOC)).getFacets(); + + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_new_lines() { + index( + // 3 docs with ncloc<1K + newDoc(NEW_LINES, 0d), + newDoc(NEW_LINES, 0d), + newDoc(NEW_LINES, 999d), + // 2 docs with ncloc>=1K and ncloc<10K + newDoc(NEW_LINES, 1_000d), + newDoc(NEW_LINES, 9_999d), + // 4 docs with ncloc>=10K and ncloc<100K + newDoc(NEW_LINES, 10_000d), + newDoc(NEW_LINES, 10_000d), + newDoc(NEW_LINES, 11_000d), + newDoc(NEW_LINES, 99_000d), + // 2 docs with ncloc>=100K and ncloc<500K + newDoc(NEW_LINES, 100_000d), + newDoc(NEW_LINES, 499_000d), + // 5 docs with ncloc>= 500K + newDoc(NEW_LINES, 500_000d), + newDoc(NEW_LINES, 100_000_000d), + newDoc(NEW_LINES, 500_000d), + newDoc(NEW_LINES, 1_000_000d), + newDoc(NEW_LINES, 100_000_000_000d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_LINES)).getFacets(); + + assertThat(facets.get(NEW_LINES)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 4L), + entry("100000.0-500000.0", 2L), + entry("500000.0-*", 5L)); + } + + @Test + public void facet_coverage() { + index( + // 1 doc with no coverage + newDocWithNoMeasure(), + // 3 docs with coverage<30% + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 29d), + // 2 docs with coverage>=30% and coverage<50% + newDoc(COVERAGE, 30d), + newDoc(COVERAGE, 49d), + // 4 docs with coverage>=50% and coverage<70% + newDoc(COVERAGE, 50d), + newDoc(COVERAGE, 60d), + newDoc(COVERAGE, 60d), + newDoc(COVERAGE, 69d), + // 2 docs with coverage>=70% and coverage<80% + newDoc(COVERAGE, 70d), + newDoc(COVERAGE, 79d), + // 5 docs with coverage>= 80% + newDoc(COVERAGE, 80d), + newDoc(COVERAGE, 80d), + newDoc(COVERAGE, 90d), + newDoc(COVERAGE, 90.5d), + newDoc(COVERAGE, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); + + assertThat(facets.get(COVERAGE)).containsOnly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 4L), + entry("70.0-80.0", 2L), + entry("80.0-*", 5L)); + } + + @Test + public void facet_coverage_is_sticky() { + index( + // docs with no coverage + newDoc(NCLOC, 999d, DUPLICATION, 0d), + newDoc(NCLOC, 999d, DUPLICATION, 1d), + newDoc(NCLOC, 999d, DUPLICATION, 20d), + // docs with coverage<30% + newDoc(NCLOC, 999d, COVERAGE, 0d, DUPLICATION, 0d), + newDoc(NCLOC, 1_000d, COVERAGE, 10d, DUPLICATION, 0d), + newDoc(NCLOC, 9_999d, COVERAGE, 20d, DUPLICATION, 0d), + // docs with coverage>=30% and coverage<50% + newDoc(NCLOC, 10_000d, COVERAGE, 31d, DUPLICATION, 0d), + newDoc(NCLOC, 11_000d, COVERAGE, 40d, DUPLICATION, 0d), + // docs with coverage>=50% and coverage<70% + newDoc(NCLOC, 99_000d, COVERAGE, 50d, DUPLICATION, 0d), + // docs with coverage>=70% and coverage<80% + newDoc(NCLOC, 100_000d, COVERAGE, 71d, DUPLICATION, 0d), + // docs with coverage>= 80% + newDoc(NCLOC, 499_000d, COVERAGE, 80d, DUPLICATION, 15d), + newDoc(NCLOC, 501_000d, COVERAGE, 810d, DUPLICATION, 20d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)) + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)), + new SearchOptions().addFacets(COVERAGE, NCLOC)).getFacets(); + + // Sticky facet on coverage does not take into account coverage filter + assertThat(facets.get(COVERAGE)).containsExactly( + entry("NO_DATA", 2L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 1L), + entry("70.0-80.0", 1L), + entry("80.0-*", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_coverage_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // 1 doc with no coverage + newDocWithNoMeasure(), + // docs with coverage<30% + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 0d), + newDoc(COVERAGE, 29d), + // docs with coverage>=30% and coverage<50% + newDoc(COVERAGE, 30d), + newDoc(COVERAGE, 49d)); + + // User cannot see these projects + indexForUser(USER2, + // 2 docs with no coverage + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // docs with coverage>=50% and coverage<70% + newDoc(COVERAGE, 50d), + // docs with coverage>=70% and coverage<80% + newDoc(COVERAGE, 70d), + // docs with coverage>= 80% + newDoc(COVERAGE, 80d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(COVERAGE)).getFacets(); + + assertThat(facets.get(COVERAGE)).containsExactly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 0L), + entry("70.0-80.0", 0L), + entry("80.0-*", 0L)); + } + + @Test + public void facet_new_coverage() { + index( + // 1 doc with no coverage + newDocWithNoMeasure(), + // 3 docs with coverage<30% + newDoc(NEW_COVERAGE, 0d), + newDoc(NEW_COVERAGE, 0d), + newDoc(NEW_COVERAGE, 29d), + // 2 docs with coverage>=30% and coverage<50% + newDoc(NEW_COVERAGE, 30d), + newDoc(NEW_COVERAGE, 49d), + // 4 docs with coverage>=50% and coverage<70% + newDoc(NEW_COVERAGE, 50d), + newDoc(NEW_COVERAGE, 60d), + newDoc(NEW_COVERAGE, 60d), + newDoc(NEW_COVERAGE, 69d), + // 2 docs with coverage>=70% and coverage<80% + newDoc(NEW_COVERAGE, 70d), + newDoc(NEW_COVERAGE, 79d), + // 5 docs with coverage>= 80% + newDoc(NEW_COVERAGE, 80d), + newDoc(NEW_COVERAGE, 80d), + newDoc(NEW_COVERAGE, 90d), + newDoc(NEW_COVERAGE, 90.5d), + newDoc(NEW_COVERAGE, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_COVERAGE)).getFacets(); + + assertThat(facets.get(NEW_COVERAGE)).containsOnly( + entry("NO_DATA", 1L), + entry("*-30.0", 3L), + entry("30.0-50.0", 2L), + entry("50.0-70.0", 4L), + entry("70.0-80.0", 2L), + entry("80.0-*", 5L)); + } + + @Test + public void facet_duplicated_lines_density() { + index( + // 1 doc with no duplication + newDocWithNoMeasure(), + // 3 docs with duplication<3% + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 2.9d), + // 2 docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d), + newDoc(DUPLICATION, 4.9d), + // 4 docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d), + newDoc(DUPLICATION, 6d), + newDoc(DUPLICATION, 6d), + newDoc(DUPLICATION, 9.9d), + // 2 docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d), + newDoc(DUPLICATION, 19.9d), + // 5 docs with duplication>= 20% + newDoc(DUPLICATION, 20d), + newDoc(DUPLICATION, 20d), + newDoc(DUPLICATION, 50d), + newDoc(DUPLICATION, 80d), + newDoc(DUPLICATION, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); + + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 4L), + entry("10.0-20.0", 2L), + entry("20.0-*", 5L)); + } + + @Test + public void facet_duplicated_lines_density_is_sticky() { + index( + // docs with no duplication + newDoc(NCLOC, 50_001d, COVERAGE, 29d), + // docs with duplication<3% + newDoc(DUPLICATION, 0d, NCLOC, 999d, COVERAGE, 0d), + // docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d, NCLOC, 5000d, COVERAGE, 0d), + newDoc(DUPLICATION, 4.9d, NCLOC, 6000d, COVERAGE, 0d), + // docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d, NCLOC, 11000d, COVERAGE, 0d), + // docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d, NCLOC, 120000d, COVERAGE, 10d), + newDoc(DUPLICATION, 19.9d, NCLOC, 130000d, COVERAGE, 20d), + // docs with duplication>= 20% + newDoc(DUPLICATION, 20d, NCLOC, 1000000d, COVERAGE, 40d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(DUPLICATION, Operator.LT, 10d)) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), + new SearchOptions().addFacets(DUPLICATION, NCLOC)).getFacets(); + + // Sticky facet on duplication does not take into account duplication filter + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 1L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 1L), + entry("10.0-20.0", 2L), + entry("20.0-*", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsOnly( + entry("*-1000.0", 1L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 1L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_duplicated_lines_density_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // docs with no duplication + newDocWithNoMeasure(), + // docs with duplication<3% + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 0d), + newDoc(DUPLICATION, 2.9d), + // docs with duplication>=3% and duplication<5% + newDoc(DUPLICATION, 3d), + newDoc(DUPLICATION, 4.9d)); + + // User cannot see these projects + indexForUser(USER2, + // docs with no duplication + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // docs with duplication>=5% and duplication<10% + newDoc(DUPLICATION, 5d), + // docs with duplication>=10% and duplication<20% + newDoc(DUPLICATION, 10d), + // docs with duplication>= 20% + newDoc(DUPLICATION, 20d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(DUPLICATION)).getFacets(); + + assertThat(facets.get(DUPLICATION)).containsOnly( + entry("NO_DATA", 1L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 0L), + entry("10.0-20.0", 0L), + entry("20.0-*", 0L)); + } + + @Test + public void facet_new_duplicated_lines_density() { + index( + // 2 docs with no measure + newDocWithNoMeasure(), + newDocWithNoMeasure(), + // 3 docs with duplication<3% + newDoc(NEW_DUPLICATION, 0d), + newDoc(NEW_DUPLICATION, 0d), + newDoc(NEW_DUPLICATION, 2.9d), + // 2 docs with duplication>=3% and duplication<5% + newDoc(NEW_DUPLICATION, 3d), + newDoc(NEW_DUPLICATION, 4.9d), + // 4 docs with duplication>=5% and duplication<10% + newDoc(NEW_DUPLICATION, 5d), + newDoc(NEW_DUPLICATION, 6d), + newDoc(NEW_DUPLICATION, 6d), + newDoc(NEW_DUPLICATION, 9.9d), + // 2 docs with duplication>=10% and duplication<20% + newDoc(NEW_DUPLICATION, 10d), + newDoc(NEW_DUPLICATION, 19.9d), + // 5 docs with duplication>= 20% + newDoc(NEW_DUPLICATION, 20d), + newDoc(NEW_DUPLICATION, 20d), + newDoc(NEW_DUPLICATION, 50d), + newDoc(NEW_DUPLICATION, 80d), + newDoc(NEW_DUPLICATION, 100d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(NEW_DUPLICATION)).getFacets(); + + assertThat(facets.get(NEW_DUPLICATION)).containsExactly( + entry("NO_DATA", 2L), + entry("*-3.0", 3L), + entry("3.0-5.0", 2L), + entry("5.0-10.0", 4L), + entry("10.0-20.0", 2L), + entry("20.0-*", 5L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating(String metricKey) { + index( + // 3 docs with rating A + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + // 2 docs with rating B + newDoc(metricKey, 2d), + newDoc(metricKey, 2d), + // 4 docs with rating C + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + newDoc(metricKey, 3d), + // 2 docs with rating D + newDoc(metricKey, 4d), + newDoc(metricKey, 4d), + // 5 docs with rating E + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d), + newDoc(metricKey, 5d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); + + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 4L), + entry("4", 2L), + entry("5", 5L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating_is_sticky(String metricKey) { + index( + // docs with rating A + newDoc(metricKey, 1d, NCLOC, 100d, COVERAGE, 0d), + newDoc(metricKey, 1d, NCLOC, 200d, COVERAGE, 0d), + newDoc(metricKey, 1d, NCLOC, 999d, COVERAGE, 0d), + // docs with rating B + newDoc(metricKey, 2d, NCLOC, 2000d, COVERAGE, 0d), + newDoc(metricKey, 2d, NCLOC, 5000d, COVERAGE, 0d), + // docs with rating C + newDoc(metricKey, 3d, NCLOC, 20000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 30000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 40000d, COVERAGE, 0d), + newDoc(metricKey, 3d, NCLOC, 50000d, COVERAGE, 0d), + // docs with rating D + newDoc(metricKey, 4d, NCLOC, 120000d, COVERAGE, 0d), + // docs with rating E + newDoc(metricKey, 5d, NCLOC, 600000d, COVERAGE, 40d), + newDoc(metricKey, 5d, NCLOC, 700000d, COVERAGE, 50d), + newDoc(metricKey, 5d, NCLOC, 800000d, COVERAGE, 60d)); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .addMetricCriterion(MetricCriterion.create(metricKey, Operator.LT, 3d)) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 30d)), + new SearchOptions().addFacets(metricKey, NCLOC)).getFacets(); + + // Sticky facet on maintainability rating does not take into account maintainability rating filter + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 4L), + entry("4", 1L), + entry("5", 0L)); + // But facet on ncloc does well take into into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 2L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + @UseDataProvider("rating_metric_keys") + public void facet_on_rating_contains_only_projects_authorized_for_user(String metricKey) { + // User can see these projects + indexForUser(USER1, + // 3 docs with rating A + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + newDoc(metricKey, 1d), + // 2 docs with rating B + newDoc(metricKey, 2d), + newDoc(metricKey, 2d)); + + // User cannot see these projects + indexForUser(USER2, + // docs with rating C + newDoc(metricKey, 3d), + // docs with rating D + newDoc(metricKey, 4d), + // docs with rating E + newDoc(metricKey, 5d)); + + userSession.logIn(USER1); + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(metricKey)).getFacets(); + + assertThat(facets.get(metricKey)).containsExactly( + entry("1", 3L), + entry("2", 2L), + entry("3", 0L), + entry("4", 0L), + entry("5", 0L)); + } + + @Test + public void facet_quality_gate() { + index( + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name()), + // 3 docs with QG WARN + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name()), + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); + + assertThat(result).containsOnly( + entry(ERROR.name(), 4L), + entry(WARN.name(), 3L), + entry(OK.name(), 2L)); + } + + @Test + public void facet_quality_gate_is_sticky() { + index( + // 2 docs with QG OK + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), + newDoc(NCLOC, 10d, COVERAGE, 0d).setQualityGateStatus(OK.name()), + // 3 docs with QG WARN + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(WARN.name()), + // 4 docs with QG ERROR + newDoc(NCLOC, 100d, COVERAGE, 0d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 5000d, COVERAGE, 40d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 12000d, COVERAGE, 50d).setQualityGateStatus(ERROR.name()), + newDoc(NCLOC, 13000d, COVERAGE, 60d).setQualityGateStatus(ERROR.name())); + + Facets facets = underTest.search(new ProjectMeasuresQuery() + .setQualityGateStatus(ERROR) + .addMetricCriterion(MetricCriterion.create(COVERAGE, Operator.LT, 55d)), + new SearchOptions().addFacets(ALERT_STATUS_KEY, NCLOC)).getFacets(); + + // Sticky facet on quality gate does not take into account quality gate filter + assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( + entry(OK.name(), 2L), + entry(WARN.name(), 3L), + entry(ERROR.name(), 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_quality_gate_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + // 2 docs with QG OK + newDoc().setQualityGateStatus(OK.name()), + newDoc().setQualityGateStatus(OK.name()), + // 3 docs with QG WARN + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name()), + newDoc().setQualityGateStatus(WARN.name())); + + // User cannot see these projects + indexForUser(USER2, + // 4 docs with QG ERROR + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name()), + newDoc().setQualityGateStatus(ERROR.name())); + + userSession.logIn(USER1); + LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(ALERT_STATUS_KEY)).getFacets().get(ALERT_STATUS_KEY); + + assertThat(result).containsOnly( + entry(ERROR.name(), 0L), + entry(WARN.name(), 3L), + entry(OK.name(), 2L)); + } + + @Test + public void facet_languages() { + index( + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(singletonList("xoo")), + newDoc().setLanguages(singletonList("xml")), + newDoc().setLanguages(asList("", "java")), + newDoc().setLanguages(asList("", "java", "xoo"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("", 2L), + entry("java", 4L), + entry("xoo", 2L), + entry("xml", 1L)); + } + + @Test + public void facet_languages_is_limited_to_10_languages() { + index( + newDoc().setLanguages(asList("", "java", "xoo", "css", "cpp")), + newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), + newDoc().setLanguages(asList("js", "scala"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).hasSize(10); + } + + @Test + public void facet_languages_is_sticky() { + index( + newDoc(NCLOC, 10d).setLanguages(singletonList("java")), + newDoc(NCLOC, 10d).setLanguages(singletonList("java")), + newDoc(NCLOC, 10d).setLanguages(singletonList("xoo")), + newDoc(NCLOC, 100d).setLanguages(singletonList("xml")), + newDoc(NCLOC, 100d).setLanguages(asList("", "java")), + newDoc(NCLOC, 5000d).setLanguages(asList("", "java", "xoo"))); + + Facets facets = underTest.search( + new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("java")), + new SearchOptions().addFacets(LANGUAGES, NCLOC)).getFacets(); + + // Sticky facet on language does not take into account language filter + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("", 2L), + entry("java", 4L), + entry("xoo", 2L), + entry("xml", 1L)); + // But facet on ncloc does well take account into filters + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", 3L), + entry("1000.0-10000.0", 1L), + entry("10000.0-100000.0", 0L), + entry("100000.0-500000.0", 0L), + entry("500000.0-*", 0L)); + } + + @Test + public void facet_languages_returns_more_than_10_languages_when_languages_filter_contains_value_not_in_top_10() { + index( + newDoc().setLanguages(asList("", "java", "xoo", "css", "cpp")), + newDoc().setLanguages(asList("xml", "php", "python", "perl", "ruby")), + newDoc().setLanguages(asList("js", "scala"))); + + Facets facets = underTest.search(new ProjectMeasuresQuery().setLanguages(ImmutableSet.of("xoo", "xml")), new SearchOptions().addFacets(LANGUAGES)).getFacets(); + + assertThat(facets.get(LANGUAGES)).containsOnly( + entry("", 1L), + entry("cpp", 1L), + entry("css", 1L), + entry("java", 1L), + entry("js", 1L), + entry("perl", 1L), + entry("php", 1L), + entry("python", 1L), + entry("ruby", 1L), + entry("scala", 1L), + entry("xoo", 1L), + entry("xml", 1L)); + } + + @Test + public void facet_languages_contains_only_projects_authorized_for_user() { + // User can see these projects + indexForUser(USER1, + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(asList("java", "xoo"))); + + // User cannot see these projects + indexForUser(USER2, + newDoc().setLanguages(singletonList("java")), + newDoc().setLanguages(asList("java", "xoo"))); + + userSession.logIn(USER1); + LinkedHashMap result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(LANGUAGES)).getFacets().get(LANGUAGES); + + assertThat(result).containsOnly( + entry("java", 2L), + entry("xoo", 1L)); + } + + @Test + public void facet_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("finance", "javascript")), + newDoc().setTags(newArrayList("marketing", "finance")), + newDoc().setTags(newArrayList("marketing", "offshore")), + newDoc().setTags(newArrayList("finance", "marketing")), + newDoc().setTags(newArrayList("finance"))); + + Map result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); + + assertThat(result).containsOnly( + entry("finance", 5L), + entry("marketing", 3L), + entry("offshore", 2L), + entry("java", 1L), + entry("javascript", 1L)); + } + + @Test + public void facet_tags_is_sticky() { + index( + newDoc().setTags(newArrayList("finance")).setQualityGateStatus(OK.name()), + newDoc().setTags(newArrayList("finance")).setQualityGateStatus(ERROR.name()), + newDoc().setTags(newArrayList("cpp")).setQualityGateStatus(WARN.name())); + + Facets facets = underTest.search( + new ProjectMeasuresQuery().setTags(newHashSet("cpp")), + new SearchOptions().addFacets(FIELD_TAGS).addFacets(ALERT_STATUS_KEY)) + .getFacets(); + + assertThat(facets.get(FIELD_TAGS)).containsOnly( + entry("finance", 2L), + entry("cpp", 1L)); + assertThat(facets.get(ALERT_STATUS_KEY)).containsOnly( + entry(OK.name(), 0L), + entry(ERROR.name(), 0L), + entry(WARN.name(), 1L)); + } + + @Test + public void facet_tags_returns_10_elements_by_default() { + index( + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("solo"))); + + Map result = underTest.search(new ProjectMeasuresQuery(), new SearchOptions().addFacets(FIELD_TAGS)).getFacets().get(FIELD_TAGS); + + assertThat(result).hasSize(10).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10"); + } + + @Test + public void facet_tags_returns_more_than_10_tags_when_tags_filter_contains_value_not_in_top_10() { + index( + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10")), + newDoc().setTags(newArrayList("solo", "solo2"))); + + Map result = underTest.search(new ProjectMeasuresQuery().setTags(ImmutableSet.of("solo", "solo2")), new SearchOptions().addFacets(FIELD_TAGS)).getFacets() + .get(FIELD_TAGS); + + assertThat(result).hasSize(12).containsOnlyKeys("finance1", "finance2", "finance3", "finance4", "finance5", "finance6", "finance7", "finance8", "finance9", "finance10", "solo", + "solo2"); + } + + @Test + public void search_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List result = underTest.searchTags("off", 10); + + assertThat(result).containsOnly("offshore", "official", "Madhoff"); + } + + @Test + public void search_tags_return_all_tags() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List result = underTest.searchTags(null, 10); + + assertThat(result).containsOnly("offshore", "official", "Madhoff", "finance", "marketing", "java", "javascript"); + } + + @Test + public void search_tags_in_lexical_order() { + index( + newDoc().setTags(newArrayList("finance", "offshore", "java")), + newDoc().setTags(newArrayList("official", "javascript")), + newDoc().setTags(newArrayList("marketing", "official")), + newDoc().setTags(newArrayList("marketing", "Madhoff")), + newDoc().setTags(newArrayList("finance", "offshore")), + newDoc().setTags(newArrayList("offshore"))); + + List result = underTest.searchTags(null, 10); + + assertThat(result).containsExactly("Madhoff", "finance", "java", "javascript", "marketing", "official", "offshore"); + } + + @Test + public void search_tags_only_of_authorized_projects() { + indexForUser(USER1, + newDoc(PROJECT1).setTags(singletonList("finance")), + newDoc(PROJECT2).setTags(singletonList("marketing"))); + indexForUser(USER2, + newDoc(PROJECT3).setTags(singletonList("offshore"))); + + userSession.logIn(USER1); + + List result = underTest.searchTags(null, 10); + + assertThat(result).containsOnly("finance", "marketing"); + } + + @Test + public void search_tags_with_no_tags() { + List result = underTest.searchTags("whatever", 10); + + assertThat(result).isEmpty(); + } + + @Test + public void search_tags_with_page_size_at_0() { + index(newDoc().setTags(newArrayList("offshore"))); + + List result = underTest.searchTags(null, 0); + + assertThat(result).isEmpty(); + } + + @Test + public void search_statistics() { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, + newDoc("lines", 10, "coverage", 80) + .setLanguages(Arrays.asList("java", "cs", "js")) + .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 200, "cs", 250, "js", 50)), + newDoc("lines", 20, "coverage", 80) + .setLanguages(Arrays.asList("java", "python", "kotlin")) + .setNclocLanguageDistributionFromMap(ImmutableMap.of("java", 300, "python", 100, "kotlin", 404))); + + ProjectMeasuresStatistics result = underTest.searchTelemetryStatistics(); + + assertThat(result.getProjectCount()).isEqualTo(2); + assertThat(result.getProjectCountByLanguage()).containsOnly( + entry("java", 2L), entry("cs", 1L), entry("js", 1L), entry("python", 1L), entry("kotlin", 1L)); + assertThat(result.getNclocByLanguage()).containsOnly( + entry("java", 500L), entry("cs", 250L), entry("js", 50L), entry("python", 100L), entry("kotlin", 404L)); + } + + @Test + public void fail_if_page_size_greater_than_500() { + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("Page size must be lower than or equals to 500"); + + underTest.searchTags("whatever", 501); + } + + private void index(ProjectMeasuresDoc... docs) { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); + for (ProjectMeasuresDoc doc : docs) { + IndexPermissions access = new IndexPermissions(doc.getId(), Qualifiers.PROJECT); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + } + } + + private void indexForUser(UserDto user, ProjectMeasuresDoc... docs) { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); + for (ProjectMeasuresDoc doc : docs) { + IndexPermissions access = new IndexPermissions(doc.getId(), Qualifiers.PROJECT); + access.addUserId(user.getId()); + authorizationIndexerTester.allow(access); + } + } + + private void indexForGroup(GroupDto group, ProjectMeasuresDoc... docs) { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); + for (ProjectMeasuresDoc doc : docs) { + IndexPermissions access = new IndexPermissions(doc.getId(), Qualifiers.PROJECT); + access.addGroupId(group.getId()); + authorizationIndexerTester.allow(access); + } + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project) { + return new ProjectMeasuresDoc() + .setOrganizationUuid(project.getOrganizationUuid()) + .setId(project.uuid()) + .setKey(project.getDbKey()) + .setName(project.name()); + } + + private static ProjectMeasuresDoc newDoc() { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2))); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1), newMeasure(metric2, value2), newMeasure(metric3, value3))); + } + + private static Map newMeasure(String key, Object value) { + return ImmutableMap.of("key", key, "value", value); + } + + private static ProjectMeasuresDoc newDocWithNoMeasure() { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG)); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2); + } + + private static ProjectMeasuresDoc newDoc(String metric1, Object value1, String metric2, Object value2, String metric3, Object value3) { + return newDoc(ComponentTesting.newPrivateProjectDto(ORG), metric1, value1, metric2, value2, metric3, value3); + } + + private void assertResults(ProjectMeasuresQuery query, ComponentDto... expectedProjects) { + List result = underTest.search(query, new SearchOptions()).getIds(); + assertThat(result).containsExactly(Arrays.stream(expectedProjects).map(ComponentDto::uuid).toArray(String[]::new)); + } + + private void assertNoResults(ProjectMeasuresQuery query) { + assertResults(query); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java new file mode 100644 index 00000000000..a60a7a860a1 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresIndexTextSearchTest.java @@ -0,0 +1,336 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import com.google.common.collect.ImmutableMap; +import java.util.List; +import java.util.Map; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.utils.System2; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.organization.OrganizationTesting; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.Facets; +import org.sonar.server.es.SearchOptions; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; +import org.sonar.server.permission.index.IndexPermissions; +import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; +import org.sonar.server.tester.UserSessionRule; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Lists.newArrayList; +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; +import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; +import static org.sonar.server.measure.index.ProjectMeasuresIndexDefinition.INDEX_TYPE_PROJECT_MEASURES; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.GT; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.LT; + +public class ProjectMeasuresIndexTextSearchTest { + + private static final String NCLOC = "ncloc"; + + private static final OrganizationDto ORG = OrganizationTesting.newOrganizationDto(); + + @Rule + public EsTester es = EsTester.create(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); + private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); + private ProjectMeasuresIndex underTest = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); + + @Test + public void match_exact_case_insensitive_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts")), + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube"))); + + assertTextQueryResults("Apache Struts", "struts"); + assertTextQueryResults("APACHE STRUTS", "struts"); + assertTextQueryResults("APACHE struTS", "struts"); + } + + @Test + public void match_from_sub_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("truts", "struts"); + assertTextQueryResults("pache", "struts"); + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("che stru", "struts"); + } + + @Test + public void match_name_with_dot() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache.Struts"))); + + assertTextQueryResults("apache struts", "struts"); + } + + @Test + public void match_partial_name() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("XstrutsxXjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word1() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("StrutsObject.java"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_prefix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.xjava"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_name_suffix_word2() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStrutsObject.xjavax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_subset_of_document_terms() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Some.Struts.Project.java.old"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void match_partial_match_prefix_and_suffix_everywhere() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("MyStruts.javax"))); + + assertTextQueryResults("struts java", "struts"); + } + + @Test + public void ignore_empty_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Struts"))); + + assertTextQueryResults(" struts \n \n\n", "struts"); + } + + @Test + public void match_name_from_prefix() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("apach", "struts"); + assertTextQueryResults("ApA", "struts"); + assertTextQueryResults("AP", "struts"); + } + + @Test + public void match_name_from_two_words() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("ApacheStrutsFoundation"))); + + assertTextQueryResults("apache struts", "project"); + assertTextQueryResults("struts apache", "project"); + // Only one word is matching + assertNoResults("apache plugin"); + assertNoResults("project struts"); + } + + @Test + public void match_long_name() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("LongNameLongNameLongNameLongNameSonarQube")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("LongNameLongNameLongNameLongNameSonarQubeX"))); + + assertTextQueryResults("LongNameLongNameLongNameLongNameSonarQube", "project1", "project2"); + } + + @Test + public void match_name_with_two_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("struts").setName("Apache Struts"))); + + assertTextQueryResults("st", "struts"); + assertTextQueryResults("tr", "struts"); + } + + @Test + public void match_exact_case_insensitive_key() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1")), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"))); + + assertTextQueryResults("project1", "project1"); + assertTextQueryResults("PROJECT1", "project1"); + assertTextQueryResults("pRoJecT1", "project1"); + } + + @Test + public void match_key_with_dot() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org.sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_dash() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org-sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org-sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org.sonarqube"); + assertNoResults("org:sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_with_colon() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org:sonarqube")), + newDoc(newPrivateProjectDto(ORG).setUuid("sq").setName("SQ").setDbKey("sonarqube"))); + + assertTextQueryResults("org:sonarqube", "sonarqube"); + assertNoResults("orgsonarqube"); + assertNoResults("org-sonarqube"); + assertNoResults("org_sonarqube"); + assertNoResults("org sonarqube"); + } + + @Test + public void match_key_having_all_special_characters() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("sonarqube").setName("SonarQube").setDbKey("org.sonarqube:sonar-sérvèr_ç"))); + + assertTextQueryResults("org.sonarqube:sonar-sérvèr_ç", "sonarqube"); + } + + @Test + public void does_not_match_partial_key() { + index(newDoc(newPrivateProjectDto(ORG).setUuid("project").setName("some name").setDbKey("theKey"))); + + assertNoResults("theke"); + assertNoResults("hekey"); + } + + @Test + public void facets_take_into_account_text_search() { + index( + // docs with ncloc<1K + newDoc(newPrivateProjectDto(ORG).setName("Windows").setDbKey("project1"), NCLOC, 0d), + newDoc(newPrivateProjectDto(ORG).setName("apachee").setDbKey("project2"), NCLOC, 999d), + // docs with ncloc>=1K and ncloc<10K + newDoc(newPrivateProjectDto(ORG).setName("Apache").setDbKey("project3"), NCLOC, 1_000d), + // docs with ncloc>=100K and ncloc<500K + newDoc(newPrivateProjectDto(ORG).setName("Apache Foundation").setDbKey("project4"), NCLOC, 100_000d)); + + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("PAch"), 1L, 1L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("apache foundation"), 0L, 0L, 0L, 1L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project3"), 0L, 1L, 0L, 0L, 0L); + assertNclocFacet(new ProjectMeasuresQuery().setQueryText("project"), 0L, 0L, 0L, 0L, 0L); + } + + @Test + public void filter_by_metric_take_into_account_text_search() { + index( + newDoc(newPrivateProjectDto(ORG).setUuid("project1").setName("Windows").setDbKey("project1"), NCLOC, 30_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project2").setName("apachee").setDbKey("project2"), NCLOC, 40_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project3").setName("Apache").setDbKey("project3"), NCLOC, 50_000d), + newDoc(newPrivateProjectDto(ORG).setUuid("project4").setName("Apache").setDbKey("project4"), NCLOC, 60_000d)); + + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d)), "project3", "project4", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apache").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("PAC").addMetricCriterion(MetricCriterion.create(NCLOC, LT, 55_000d)), "project3", "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("apachee").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 30_000d)), "project2"); + assertResults(new ProjectMeasuresQuery().setQueryText("unknown").addMetricCriterion(MetricCriterion.create(NCLOC, GT, 20_000d))); + } + + private void index(ProjectMeasuresDoc... docs) { + es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); + stream(docs).forEach(doc -> { + IndexPermissions access = new IndexPermissions(doc.getId(), Qualifiers.PROJECT); + access.allowAnyone(); + authorizationIndexerTester.allow(access); + }); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project) { + return new ProjectMeasuresDoc() + .setOrganizationUuid(project.getOrganizationUuid()) + .setId(project.uuid()) + .setKey(project.getDbKey()) + .setName(project.name()); + } + + private static ProjectMeasuresDoc newDoc(ComponentDto project, String metric1, Object value1) { + return newDoc(project).setMeasures(newArrayList(newMeasure(metric1, value1))); + } + + private static Map newMeasure(String key, Object value) { + return ImmutableMap.of("key", key, "value", value); + } + + private void assertResults(ProjectMeasuresQuery query, String... expectedProjectUuids) { + List result = underTest.search(query, new SearchOptions()).getIds(); + assertThat(result).containsExactly(expectedProjectUuids); + } + + private void assertTextQueryResults(String queryText, String... expectedProjectUuids) { + assertResults(new ProjectMeasuresQuery().setQueryText(queryText), expectedProjectUuids); + } + + private void assertNoResults(String queryText) { + assertTextQueryResults(queryText); + } + + private void assertNclocFacet(ProjectMeasuresQuery query, Long... facetExpectedValues) { + checkArgument(facetExpectedValues.length == 5, "5 facet values is required"); + Facets facets = underTest.search(query, new SearchOptions().addFacets(NCLOC)).getFacets(); + assertThat(facets.get(NCLOC)).containsExactly( + entry("*-1000.0", facetExpectedValues[0]), + entry("1000.0-10000.0", facetExpectedValues[1]), + entry("10000.0-100000.0", facetExpectedValues[2]), + entry("100000.0-500000.0", facetExpectedValues[3]), + entry("500000.0-*", facetExpectedValues[4])); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java new file mode 100644 index 00000000000..6da1e29c8eb --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectMeasuresQueryTest.java @@ -0,0 +1,109 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.measures.Metric.Level; +import org.sonar.server.measure.index.ProjectMeasuresQuery.MetricCriterion; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; +import static org.sonar.api.measures.Metric.Level.OK; +import static org.sonar.server.measure.index.ProjectMeasuresQuery.Operator.EQ; + +public class ProjectMeasuresQueryTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private ProjectMeasuresQuery underTest = new ProjectMeasuresQuery(); + + @Test + public void empty_query() { + assertThat(underTest.getMetricCriteria()).isEmpty(); + assertThat(underTest.getQualityGateStatus()).isEmpty(); + assertThat(underTest.getOrganizationUuid()).isEmpty(); + } + + @Test + public void add_metric_criterion() { + underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::getValue) + .containsOnly(tuple("coverage", EQ, 10d)); + } + + @Test + public void isNoData_returns_true_when_no_data() { + underTest.addMetricCriterion(MetricCriterion.createNoData("coverage")); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::isNoData) + .containsOnly(tuple("coverage", true)); + } + + @Test + public void isNoData_returns_false_when_data_exists() { + underTest.addMetricCriterion(MetricCriterion.create("coverage", EQ, 10d)); + + assertThat(underTest.getMetricCriteria()) + .extracting(MetricCriterion::getMetricKey, MetricCriterion::getOperator, MetricCriterion::isNoData) + .containsOnly(tuple("coverage", EQ, false)); + } + + @Test + public void set_quality_gate_status() { + underTest.setQualityGateStatus(OK); + + assertThat(underTest.getQualityGateStatus().get()).isEqualTo(Level.OK); + } + + @Test + public void default_sort_is_by_name() { + assertThat(underTest.getSort()).isEqualTo("name"); + } + + @Test + public void fail_to_set_null_sort() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("Sort cannot be null"); + + underTest.setSort(null); + } + + @Test + public void fail_to_get_value_when_no_data() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("The criterion for metric coverage has no data"); + + MetricCriterion.createNoData("coverage").getValue(); + } + + @Test + public void fail_to_get_operator_when_no_data() { + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("The criterion for metric coverage has no data"); + + MetricCriterion.createNoData("coverage").getOperator(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java new file mode 100644 index 00000000000..0e8613fdf4e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/measure/index/ProjectsEsModuleTest.java @@ -0,0 +1,35 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.measure.index; + +import org.junit.Test; +import org.sonar.core.platform.ComponentContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.platform.ComponentContainer.COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER; + +public class ProjectsEsModuleTest { + @Test + public void verify_count_of_added_components() { + ComponentContainer container = new ComponentContainer(); + new ProjectsEsModule().configure(container); + assertThat(container.size()).isEqualTo(3 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndex.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndex.java new file mode 100644 index 00000000000..d8c93720ca9 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndex.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.Arrays; +import java.util.List; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.SearchHits; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.server.es.EsClient; + +import static org.sonar.server.permission.index.FooIndexDefinition.FOO_INDEX; +import static org.sonar.server.permission.index.FooIndexDefinition.FOO_TYPE; + +public class FooIndex { + + private final EsClient esClient; + private final WebAuthorizationTypeSupport authorizationTypeSupport; + + public FooIndex(EsClient esClient, WebAuthorizationTypeSupport authorizationTypeSupport) { + this.esClient = esClient; + this.authorizationTypeSupport = authorizationTypeSupport; + } + + public boolean hasAccessToProject(String projectUuid) { + SearchHits hits = esClient.prepareSearch(FOO_INDEX) + .setTypes(FOO_TYPE) + .setQuery(QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery(FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) + .filter(authorizationTypeSupport.createQueryFilter())) + .get() + .getHits(); + List names = Arrays.stream(hits.hits()) + .map(h -> h.getSource().get(FooIndexDefinition.FIELD_NAME).toString()) + .collect(MoreCollectors.toList()); + return names.size() == 2 && names.contains("bar") && names.contains("baz"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java new file mode 100644 index 00000000000..fa3d294e48a --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexDefinition.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import org.sonar.api.config.internal.MapSettings; +import org.sonar.server.es.IndexDefinition; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.NewIndex; + +import static org.sonar.server.es.NewIndex.SettingsConfiguration.MANUAL_REFRESH_INTERVAL; +import static org.sonar.server.es.NewIndex.SettingsConfiguration.newBuilder; + +public class FooIndexDefinition implements IndexDefinition { + + public static final String FOO_INDEX = "foos"; + public static final String FOO_TYPE = "foo"; + public static final IndexType INDEX_TYPE_FOO = new IndexType(FOO_INDEX, FOO_TYPE); + public static final String FIELD_NAME = "name"; + public static final String FIELD_PROJECT_UUID = "projectUuid"; + + @Override + public void define(IndexDefinitionContext context) { + NewIndex index = context.create(FOO_INDEX, newBuilder(new MapSettings().asConfig()).setRefreshInterval(MANUAL_REFRESH_INTERVAL).build()); + + NewIndex.NewIndexType type = index.createType(FOO_TYPE) + .requireProjectAuthorization(); + + type.keywordFieldBuilder(FIELD_NAME).build(); + type.keywordFieldBuilder(FIELD_PROJECT_UUID).build(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java new file mode 100644 index 00000000000..f5122681075 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/FooIndexer.java @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import java.util.Set; +import org.sonar.db.DbSession; +import org.sonar.db.es.EsQueueDto; +import org.sonar.server.es.EsClient; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.IndexingResult; +import org.sonar.server.es.ProjectIndexer; + +import static org.sonar.server.permission.index.FooIndexDefinition.INDEX_TYPE_FOO; + +public class FooIndexer implements ProjectIndexer, NeedAuthorizationIndexer { + + private static final AuthorizationScope AUTHORIZATION_SCOPE = new AuthorizationScope(INDEX_TYPE_FOO, p -> true); + + private final EsClient esClient; + + public FooIndexer(EsClient esClient) { + this.esClient = esClient; + } + + @Override + public AuthorizationScope getAuthorizationScope() { + return AUTHORIZATION_SCOPE; + } + + @Override + public void indexOnAnalysis(String branchUuid) { + addToIndex(branchUuid, "bar"); + addToIndex(branchUuid, "baz"); + } + + @Override + public Collection prepareForRecovery(DbSession dbSession, Collection projectUuids, Cause cause) { + throw new UnsupportedOperationException(); + } + + private void addToIndex(String projectUuid, String name) { + esClient.prepareIndex(INDEX_TYPE_FOO) + .setRouting(projectUuid) + .setParent(projectUuid) + .setSource(ImmutableMap.of( + FooIndexDefinition.FIELD_NAME, name, + FooIndexDefinition.FIELD_PROJECT_UUID, projectUuid)) + .get(); + } + + @Override + public void indexOnStartup(Set uninitializedIndexTypes) { + throw new UnsupportedOperationException(); + } + + @Override + public Set getIndexTypes() { + return ImmutableSet.of(INDEX_TYPE_FOO); + } + + @Override + public IndexingResult index(DbSession dbSession, Collection items) { + throw new UnsupportedOperationException(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java new file mode 100644 index 00000000000..df045f6eb79 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerDaoTest.java @@ -0,0 +1,274 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.utils.System2; +import org.sonar.core.util.stream.MoreCollectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.permission.GroupPermissionDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDbTester; +import org.sonar.db.user.UserDto; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.resources.Qualifiers.APP; +import static org.sonar.api.resources.Qualifiers.PROJECT; +import static org.sonar.api.resources.Qualifiers.VIEW; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.api.web.UserRole.USER; + +public class PermissionIndexerDaoTest { + + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private DbClient dbClient = dbTester.getDbClient(); + private DbSession dbSession = dbTester.getSession(); + + private ComponentDbTester componentDbTester = new ComponentDbTester(dbTester); + private UserDbTester userDbTester = new UserDbTester(dbTester); + + private OrganizationDto organization; + private ComponentDto publicProject; + private ComponentDto privateProject1; + private ComponentDto privateProject2; + private ComponentDto view1; + private ComponentDto view2; + private ComponentDto application; + private UserDto user1; + private UserDto user2; + private GroupDto group; + + private PermissionIndexerDao underTest = new PermissionIndexerDao(); + + @Before + public void setUp() { + organization = dbTester.organizations().insert(); + publicProject = componentDbTester.insertPublicProject(organization); + privateProject1 = componentDbTester.insertPrivateProject(organization); + privateProject2 = componentDbTester.insertPrivateProject(organization); + view1 = componentDbTester.insertView(organization); + view2 = componentDbTester.insertView(organization); + application = componentDbTester.insertApplication(organization); + user1 = userDbTester.insertUser(); + user2 = userDbTester.insertUser(); + group = userDbTester.insertGroup(organization); + } + + @Test + public void select_all() { + insertTestDataForProjectsAndViews(); + + Collection dtos = underTest.selectAll(dbClient, dbSession); + Assertions.assertThat(dtos).hasSize(6); + + IndexPermissions publicProjectAuthorization = getByProjectUuid(publicProject.uuid(), dtos); + isPublic(publicProjectAuthorization, PROJECT); + + IndexPermissions view1Authorization = getByProjectUuid(view1.uuid(), dtos); + isPublic(view1Authorization, VIEW); + + IndexPermissions applicationAuthorization = getByProjectUuid(application.uuid(), dtos); + isPublic(applicationAuthorization, APP); + + IndexPermissions privateProject1Authorization = getByProjectUuid(privateProject1.uuid(), dtos); + assertThat(privateProject1Authorization.getGroupIds()).containsOnly(group.getId()); + assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject1Authorization.getUserIds()).containsOnly(user1.getId(), user2.getId()); + assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions privateProject2Authorization = getByProjectUuid(privateProject2.uuid(), dtos); + assertThat(privateProject2Authorization.getGroupIds()).isEmpty(); + assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject2Authorization.getUserIds()).containsOnly(user1.getId()); + assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions view2Authorization = getByProjectUuid(view2.uuid(), dtos); + isPublic(view2Authorization, VIEW); + } + + @Test + public void selectByUuids() { + insertTestDataForProjectsAndViews(); + + Map dtos = underTest + .selectByUuids(dbClient, dbSession, asList(publicProject.uuid(), privateProject1.uuid(), privateProject2.uuid(), view1.uuid(), view2.uuid(), application.uuid())) + .stream() + .collect(MoreCollectors.uniqueIndex(IndexPermissions::getProjectUuid, Function.identity())); + Assertions.assertThat(dtos).hasSize(6); + + IndexPermissions publicProjectAuthorization = dtos.get(publicProject.uuid()); + isPublic(publicProjectAuthorization, PROJECT); + + IndexPermissions view1Authorization = dtos.get(view1.uuid()); + isPublic(view1Authorization, VIEW); + + IndexPermissions applicationAuthorization = dtos.get(application.uuid()); + isPublic(applicationAuthorization, APP); + + IndexPermissions privateProject1Authorization = dtos.get(privateProject1.uuid()); + assertThat(privateProject1Authorization.getGroupIds()).containsOnly(group.getId()); + assertThat(privateProject1Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject1Authorization.getUserIds()).containsOnly(user1.getId(), user2.getId()); + assertThat(privateProject1Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions privateProject2Authorization = dtos.get(privateProject2.uuid()); + assertThat(privateProject2Authorization.getGroupIds()).isEmpty(); + assertThat(privateProject2Authorization.isAllowAnyone()).isFalse(); + assertThat(privateProject2Authorization.getUserIds()).containsOnly(user1.getId()); + assertThat(privateProject2Authorization.getQualifier()).isEqualTo(PROJECT); + + IndexPermissions view2Authorization = dtos.get(view2.uuid()); + isPublic(view2Authorization, VIEW); + } + + @Test + public void selectByUuids_returns_empty_list_when_project_does_not_exist() { + insertTestDataForProjectsAndViews(); + + List dtos = underTest.selectByUuids(dbClient, dbSession, asList("missing")); + Assertions.assertThat(dtos).isEmpty(); + } + + @Test + public void select_by_projects_with_high_number_of_projects() { + List projectUuids = new ArrayList<>(); + for (int i = 0; i < 350; i++) { + ComponentDto project = ComponentTesting.newPrivateProjectDto(organization, Integer.toString(i)); + dbClient.componentDao().insert(dbSession, project); + projectUuids.add(project.uuid()); + GroupPermissionDto dto = new GroupPermissionDto() + .setOrganizationUuid(group.getOrganizationUuid()) + .setGroupId(group.getId()) + .setRole(USER) + .setResourceId(project.getId()); + dbClient.groupPermissionDao().insert(dbSession, dto); + } + dbSession.commit(); + + assertThat(underTest.selectByUuids(dbClient, dbSession, projectUuids)) + .hasSize(350) + .extracting(IndexPermissions::getProjectUuid) + .containsAll(projectUuids); + } + + @Test + public void return_private_project_without_any_permission_when_no_permission_in_DB() { + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); + + // no permissions + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupIds()).isEmpty(); + assertThat(dto.getUserIds()).isEmpty(); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); + } + + @Test + public void return_public_project_with_only_AllowAnyone_true_when_no_permission_in_DB() { + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(publicProject.uuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupIds()).isEmpty(); + assertThat(dto.getUserIds()).isEmpty(); + assertThat(dto.isAllowAnyone()).isTrue(); + assertThat(dto.getProjectUuid()).isEqualTo(publicProject.uuid()); + assertThat(dto.getQualifier()).isEqualTo(publicProject.qualifier()); + } + + @Test + public void return_private_project_with_AllowAnyone_false_and_user_id_when_user_is_granted_USER_permission_directly() { + dbTester.users().insertProjectPermissionOnUser(user1, USER, privateProject1); + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupIds()).isEmpty(); + assertThat(dto.getUserIds()).containsOnly(user1.getId()); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); + } + + @Test + public void return_private_project_with_AllowAnyone_false_and_group_id_but_not_user_id_when_user_is_granted_USER_permission_through_group() { + dbTester.users().insertMember(group, user1); + dbTester.users().insertProjectPermissionOnGroup(group, USER, privateProject1); + List dtos = underTest.selectByUuids(dbClient, dbSession, singletonList(privateProject1.uuid())); + + Assertions.assertThat(dtos).hasSize(1); + IndexPermissions dto = dtos.get(0); + assertThat(dto.getGroupIds()).containsOnly(group.getId()); + assertThat(dto.getUserIds()).isEmpty(); + assertThat(dto.isAllowAnyone()).isFalse(); + assertThat(dto.getProjectUuid()).isEqualTo(privateProject1.uuid()); + assertThat(dto.getQualifier()).isEqualTo(privateProject1.qualifier()); + } + + private void isPublic(IndexPermissions view1Authorization, String qualifier) { + assertThat(view1Authorization.getGroupIds()).isEmpty(); + assertThat(view1Authorization.isAllowAnyone()).isTrue(); + assertThat(view1Authorization.getUserIds()).isEmpty(); + assertThat(view1Authorization.getQualifier()).isEqualTo(qualifier); + } + + private static IndexPermissions getByProjectUuid(String projectUuid, Collection dtos) { + return dtos.stream().filter(dto -> dto.getProjectUuid().equals(projectUuid)).findFirst().orElseThrow(IllegalArgumentException::new); + } + + private void insertTestDataForProjectsAndViews() { + // user1 has USER access on both private projects + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, publicProject); + userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject1); + userDbTester.insertProjectPermissionOnUser(user1, USER, privateProject2); + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, view1); + userDbTester.insertProjectPermissionOnUser(user1, ADMIN, application); + + // user2 has USER access on privateProject1 only + userDbTester.insertProjectPermissionOnUser(user2, USER, privateProject1); + userDbTester.insertProjectPermissionOnUser(user2, ADMIN, privateProject2); + + // group1 has USER access on privateProject1 only + userDbTester.insertProjectPermissionOnGroup(group, USER, privateProject1); + userDbTester.insertProjectPermissionOnGroup(group, ADMIN, privateProject1); + userDbTester.insertProjectPermissionOnGroup(group, ADMIN, view1); + userDbTester.insertProjectPermissionOnGroup(group, ADMIN, application); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java new file mode 100644 index 00000000000..104ce336492 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTest.java @@ -0,0 +1,431 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.Collection; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.utils.System2; +import org.sonar.db.DbSession; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.es.EsQueueDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; +import org.sonar.server.es.IndexType; +import org.sonar.server.es.IndexingResult; +import org.sonar.server.es.ProjectIndexer; +import org.sonar.server.tester.UserSessionRule; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.web.UserRole.ADMIN; +import static org.sonar.api.web.UserRole.USER; +import static org.sonar.server.es.ProjectIndexer.Cause.PERMISSION_CHANGE; +import static org.sonar.server.permission.index.IndexAuthorizationConstants.TYPE_AUTHORIZATION; + +public class PermissionIndexerTest { + + private static final IndexType INDEX_TYPE_FOO_AUTH = new IndexType(FooIndexDefinition.INDEX_TYPE_FOO.getIndex(), TYPE_AUTHORIZATION); + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Rule + public DbTester db = DbTester.create(System2.INSTANCE); + @Rule + public EsTester es = EsTester.createCustom(new FooIndexDefinition()); + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private FooIndex fooIndex = new FooIndex(es.client(), new WebAuthorizationTypeSupport(userSession)); + private FooIndexer fooIndexer = new FooIndexer(es.client()); + private PermissionIndexer underTest = new PermissionIndexer(db.getDbClient(), es.client(), fooIndexer); + + @Test + public void indexOnStartup_grants_access_to_any_user_and_to_group_Anyone_on_public_projects() { + ComponentDto project = createAndIndexPublicProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + + indexOnStartup(); + + verifyAnyoneAuthorized(project); + verifyAuthorized(project, user1); + verifyAuthorized(project, user2); + } + + @Test + public void deletion_resilience_will_deindex_projects() { + ComponentDto project1 = createUnindexedPublicProject(); + ComponentDto project2 = createUnindexedPublicProject(); + // UserDto user1 = db.users().insertUser(); + indexOnStartup(); + assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(2); + + // Simulate a indexation issue + db.getDbClient().componentDao().delete(db.getSession(), project1.getId()); + underTest.prepareForRecovery(db.getSession(), asList(project1.uuid()), ProjectIndexer.Cause.PROJECT_DELETION); + assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(1); + Collection esQueueDtos = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), Long.MAX_VALUE, 2); + + underTest.index(db.getSession(), esQueueDtos); + + assertThat(db.countRowsOfTable(db.getSession(), "es_queue")).isEqualTo(0); + assertThat(es.countDocuments(INDEX_TYPE_FOO_AUTH)).isEqualTo(1); + } + + @Test + public void indexOnStartup_grants_access_to_user() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + db.users().insertProjectPermissionOnUser(user2, ADMIN, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // user1 has access + verifyAuthorized(project, user1); + + // user2 has not access (only USER permission is accepted) + verifyNotAuthorized(project, user2); + } + + @Test + public void indexOnStartup_grants_access_to_group_on_private_project() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + UserDto user3 = db.users().insertUser(); + GroupDto group1 = db.users().insertGroup(); + GroupDto group2 = db.users().insertGroup(); + db.users().insertProjectPermissionOnGroup(group1, USER, project); + db.users().insertProjectPermissionOnGroup(group2, ADMIN, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // group1 has access + verifyAuthorized(project, user1, group1); + + // group2 has not access (only USER permission is accepted) + verifyNotAuthorized(project, user2, group2); + + // user3 is not in any group + verifyNotAuthorized(project, user3); + } + + @Test + public void indexOnStartup_grants_access_to_user_and_group() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + db.users().insertMember(group, user2); + db.users().insertProjectPermissionOnUser(user1, USER, project); + db.users().insertProjectPermissionOnGroup(group, USER, project); + + indexOnStartup(); + + // anonymous + verifyAnyoneNotAuthorized(project); + + // has direct access + verifyAuthorized(project, user1); + + // has access through group + verifyAuthorized(project, user1, group); + + // no access + verifyNotAuthorized(project, user2); + } + + @Test + public void indexOnStartup_does_not_grant_access_to_anybody_on_private_project() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneNotAuthorized(project); + verifyNotAuthorized(project, user); + verifyNotAuthorized(project, user, group); + } + + @Test + public void indexOnStartup_grants_access_to_anybody_on_public_project() { + ComponentDto project = createAndIndexPublicProject(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneAuthorized(project); + verifyAuthorized(project, user); + verifyAuthorized(project, user, group); + } + + @Test + public void indexOnStartup_grants_access_to_anybody_on_view() { + ComponentDto view = createAndIndexView(); + UserDto user = db.users().insertUser(); + GroupDto group = db.users().insertGroup(); + + indexOnStartup(); + + verifyAnyoneAuthorized(view); + verifyAuthorized(view, user); + verifyAuthorized(view, user, group); + } + + @Test + public void indexOnStartup_grants_access_on_many_projects() { + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + ComponentDto project = null; + for (int i = 0; i < 10; i++) { + project = createAndIndexPrivateProject(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + } + + indexOnStartup(); + + verifyAnyoneNotAuthorized(project); + verifyAuthorized(project, user1); + verifyNotAuthorized(project, user2); + } + + @Test + public void public_projects_are_visible_to_anybody_whatever_the_organization() { + ComponentDto projectOnOrg1 = createAndIndexPublicProject(db.organizations().insert()); + ComponentDto projectOnOrg2 = createAndIndexPublicProject(db.organizations().insert()); + UserDto user = db.users().insertUser(); + + indexOnStartup(); + + verifyAnyoneAuthorized(projectOnOrg1); + verifyAnyoneAuthorized(projectOnOrg2); + verifyAuthorized(projectOnOrg1, user); + verifyAuthorized(projectOnOrg2, user); + } + + @Test + public void indexOnAnalysis_does_nothing_because_CE_does_not_touch_permissions() { + ComponentDto project = createAndIndexPublicProject(); + + underTest.indexOnAnalysis(project.uuid()); + + assertThatAuthIndexHasSize(0); + verifyAnyoneNotAuthorized(project); + } + + @Test + public void permissions_are_not_updated_on_project_tags_update() { + ComponentDto project = createAndIndexPublicProject(); + + indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE); + + assertThatAuthIndexHasSize(0); + verifyAnyoneNotAuthorized(project); + } + + @Test + public void permissions_are_not_updated_on_project_key_update() { + ComponentDto project = createAndIndexPublicProject(); + + indexPermissions(project, ProjectIndexer.Cause.PROJECT_TAGS_UPDATE); + + assertThatAuthIndexHasSize(0); + verifyAnyoneNotAuthorized(project); + } + + @Test + public void index_permissions_on_project_creation() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user, USER, project); + + indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); + + assertThatAuthIndexHasSize(1); + verifyAuthorized(project, user); + } + + @Test + public void index_permissions_on_permission_change() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user1 = db.users().insertUser(); + UserDto user2 = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user1, USER, project); + indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); + verifyAuthorized(project, user1); + verifyNotAuthorized(project, user2); + + db.users().insertProjectPermissionOnUser(user2, USER, project); + indexPermissions(project, PERMISSION_CHANGE); + + verifyAuthorized(project, user1); + verifyAuthorized(project, user1); + } + + @Test + public void delete_permissions_on_project_deletion() { + ComponentDto project = createAndIndexPrivateProject(); + UserDto user = db.users().insertUser(); + db.users().insertProjectPermissionOnUser(user, USER, project); + indexPermissions(project, ProjectIndexer.Cause.PROJECT_CREATION); + verifyAuthorized(project, user); + + db.getDbClient().componentDao().delete(db.getSession(), project.getId()); + indexPermissions(project, ProjectIndexer.Cause.PROJECT_DELETION); + + verifyNotAuthorized(project, user); + assertThatAuthIndexHasSize(0); + } + + @Test + public void errors_during_indexing_are_recovered() { + ComponentDto project = createAndIndexPublicProject(); + es.lockWrites(INDEX_TYPE_FOO_AUTH); + + IndexingResult result = indexPermissions(project, PERMISSION_CHANGE); + assertThat(result.getTotal()).isEqualTo(1L); + assertThat(result.getFailures()).isEqualTo(1L); + + // index is still read-only, fail to recover + result = recover(); + assertThat(result.getTotal()).isEqualTo(1L); + assertThat(result.getFailures()).isEqualTo(1L); + assertThatAuthIndexHasSize(0); + assertThatEsQueueTableHasSize(1); + + es.unlockWrites(INDEX_TYPE_FOO_AUTH); + + result = recover(); + assertThat(result.getTotal()).isEqualTo(1L); + assertThat(result.getFailures()).isEqualTo(0L); + verifyAnyoneAuthorized(project); + assertThatEsQueueTableHasSize(0); + } + + private void assertThatAuthIndexHasSize(int expectedSize) { + IndexType authIndexType = underTest.getIndexTypes().iterator().next(); + assertThat(es.countDocuments(authIndexType)).isEqualTo(expectedSize); + } + + private void indexOnStartup() { + underTest.indexOnStartup(underTest.getIndexTypes()); + } + + private void verifyAuthorized(ComponentDto project, UserDto user) { + logIn(user); + verifyAuthorized(project, true); + } + + private void verifyAuthorized(ComponentDto project, UserDto user, GroupDto group) { + logIn(user).setGroups(group); + verifyAuthorized(project, true); + } + + private void verifyNotAuthorized(ComponentDto project, UserDto user) { + logIn(user); + verifyAuthorized(project, false); + } + + private void verifyNotAuthorized(ComponentDto project, UserDto user, GroupDto group) { + logIn(user).setGroups(group); + verifyAuthorized(project, false); + } + + private void verifyAnyoneAuthorized(ComponentDto project) { + userSession.anonymous(); + verifyAuthorized(project, true); + } + + private void verifyAnyoneNotAuthorized(ComponentDto project) { + userSession.anonymous(); + verifyAuthorized(project, false); + } + + private void verifyAuthorized(ComponentDto project, boolean expectedAccess) { + assertThat(fooIndex.hasAccessToProject(project.uuid())).isEqualTo(expectedAccess); + } + + private UserSessionRule logIn(UserDto u) { + userSession.logIn(u.getLogin()).setUserId(u.getId()); + return userSession; + } + + private IndexingResult indexPermissions(ComponentDto project, ProjectIndexer.Cause cause) { + DbSession dbSession = db.getSession(); + Collection items = underTest.prepareForRecovery(dbSession, singletonList(project.uuid()), cause); + dbSession.commit(); + return underTest.index(dbSession, items); + } + + private ComponentDto createUnindexedPublicProject() { + ComponentDto project = db.components().insertPublicProject(); + return project; + } + + private ComponentDto createAndIndexPrivateProject() { + ComponentDto project = db.components().insertPrivateProject(); + fooIndexer.indexOnAnalysis(project.uuid()); + return project; + } + + private ComponentDto createAndIndexPublicProject() { + ComponentDto project = db.components().insertPublicProject(); + fooIndexer.indexOnAnalysis(project.uuid()); + return project; + } + + private ComponentDto createAndIndexView() { + ComponentDto view = db.components().insertView(); + fooIndexer.indexOnAnalysis(view.uuid()); + return view; + } + + private ComponentDto createAndIndexPublicProject(OrganizationDto org) { + ComponentDto project = db.components().insertPublicProject(org); + fooIndexer.indexOnAnalysis(project.uuid()); + return project; + } + + private IndexingResult recover() { + Collection items = db.getDbClient().esQueueDao().selectForRecovery(db.getSession(), System.currentTimeMillis() + 1_000L, 10); + return underTest.index(db.getSession(), items); + } + + private void assertThatEsQueueTableHasSize(int expectedSize) { + assertThat(db.countRowsOfTable("es_queue")).isEqualTo(expectedSize); + } + +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java new file mode 100644 index 00000000000..efe190311ba --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/PermissionIndexerTester.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import java.util.Arrays; +import java.util.stream.Stream; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.UserDto; +import org.sonar.server.es.EsTester; + +import static java.util.Arrays.asList; + +public class PermissionIndexerTester { + + private final PermissionIndexer permissionIndexer; + + public PermissionIndexerTester(EsTester esTester, NeedAuthorizationIndexer indexer, NeedAuthorizationIndexer... others) { + NeedAuthorizationIndexer[] indexers = Stream.concat(Stream.of(indexer), Arrays.stream(others)).toArray(NeedAuthorizationIndexer[]::new); + this.permissionIndexer = new PermissionIndexer(null, esTester.client(), indexers); + } + + public PermissionIndexerTester allowOnlyAnyone(ComponentDto project) { + IndexPermissions dto = new IndexPermissions(project.uuid(), project.qualifier()); + dto.allowAnyone(); + permissionIndexer.index(asList(dto)); + return this; + } + + public PermissionIndexerTester allowOnlyUser(ComponentDto project, UserDto user) { + IndexPermissions dto = new IndexPermissions(project.uuid(), project.qualifier()) + .addUserId(user.getId()); + permissionIndexer.index(asList(dto)); + return this; + } + + public PermissionIndexerTester allowOnlyGroup(ComponentDto project, GroupDto group) { + IndexPermissions dto = new IndexPermissions(project.uuid(), project.qualifier()) + .addGroupId(group.getId()); + permissionIndexer.index(asList(dto)); + return this; + } + + public PermissionIndexerTester allow(IndexPermissions access) { + permissionIndexer.index(asList(access)); + return this; + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/permission/index/WebAuthorizationTypeSupportTest.java b/server/sonar-server/src/test/java/org/sonar/server/permission/index/WebAuthorizationTypeSupportTest.java new file mode 100644 index 00000000000..ab4d0168e72 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/permission/index/WebAuthorizationTypeSupportTest.java @@ -0,0 +1,153 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.permission.index; + +import org.elasticsearch.index.query.MatchAllQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.join.query.HasParentQueryBuilder; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.user.GroupDto; +import org.sonar.db.user.GroupTesting; +import org.sonar.server.tester.UserSessionRule; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.test.JsonAssert.assertJson; + +public class WebAuthorizationTypeSupportTest { + + @Rule + public UserSessionRule userSession = UserSessionRule.standalone(); + + private WebAuthorizationTypeSupport underTest = new WebAuthorizationTypeSupport(userSession); + + @Test + public void createQueryFilter_does_not_include_permission_filters_if_user_is_flagged_as_root() { + userSession.logIn().setRoot(); + + QueryBuilder filter = underTest.createQueryFilter(); + + assertThat(filter).isInstanceOf(MatchAllQueryBuilder.class); + } + + @Test + public void createQueryFilter_sets_filter_on_anyone_group_if_user_is_anonymous() { + userSession.anonymous(); + + HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); + + assertJson(filter.toString()).isSimilarTo("{" + + " \"has_parent\" : {" + + " \"query\" : {" + + " \"bool\" : {" + + " \"filter\" : [{" + + " \"bool\" : {" + + " \"should\" : [{" + + " \"term\" : {" + + " \"allowAnyone\" : {\"value\": true}" + + " }" + + " }]" + + " }" + + " }]" + + " }" + + " }," + + " \"parent_type\" : \"authorization\"" + + " }" + + "}"); + } + + @Test + public void createQueryFilter_sets_filter_on_anyone_and_user_id_if_user_is_logged_in_but_has_no_groups() { + userSession.logIn().setUserId(1234); + + HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); + + assertJson(filter.toString()).isSimilarTo("{" + + " \"has_parent\": {" + + " \"query\": {" + + " \"bool\": {" + + " \"filter\": [{" + + " \"bool\": {" + + " \"should\": [" + + " {" + + " \"term\": {" + + " \"allowAnyone\": {\"value\": true}" + + " }" + + " }," + + " {" + + " \"term\": {" + + " \"userIds\": {\"value\": 1234}" + + " }" + + " }" + + " ]" + + " }" + + " }]" + + " }" + + " }," + + " \"parent_type\": \"authorization\"" + + " }" + + "}"); + } + + @Test + public void createQueryFilter_sets_filter_on_anyone_and_user_id_and_group_ids_if_user_is_logged_in_and_has_groups() { + GroupDto group1 = GroupTesting.newGroupDto().setId(10); + GroupDto group2 = GroupTesting.newGroupDto().setId(11); + userSession.logIn().setUserId(1234).setGroups(group1, group2); + + HasParentQueryBuilder filter = (HasParentQueryBuilder) underTest.createQueryFilter(); + + assertJson(filter.toString()).isSimilarTo("{" + + " \"has_parent\": {" + + " \"query\": {" + + " \"bool\": {" + + " \"filter\": [{" + + " \"bool\": {" + + " \"should\": [" + + " {" + + " \"term\": {" + + " \"allowAnyone\": {\"value\": true}" + + " }" + + " }," + + " {" + + " \"term\": {" + + " \"userIds\": {\"value\": 1234}" + + " }" + + " }," + + " {" + + " \"term\": {" + + " \"groupIds\": {\"value\": 10}" + + " }" + + " }," + + " {" + + " \"term\": {" + + " \"groupIds\": {\"value\": 11}" + + " }" + + " }" + + " ]" + + " }" + + " }]" + + " }" + + " }," + + " \"parent_type\": \"authorization\"" + + " }" + + "}"); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java index b186377f564..c86710a8097 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/projecttag/ws/SearchActionTest.java @@ -34,9 +34,9 @@ import org.sonar.server.es.EsTester; import org.sonar.server.measure.index.ProjectMeasuresDoc; import org.sonar.server.measure.index.ProjectMeasuresIndex; import org.sonar.server.measure.index.ProjectMeasuresIndexer; -import org.sonar.server.permission.index.AuthorizationTypeSupport; -import org.sonar.server.permission.index.PermissionIndexerDao; +import org.sonar.server.permission.index.IndexPermissions; import org.sonar.server.permission.index.PermissionIndexerTester; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.TestRequest; import org.sonar.server.ws.WsActionTester; @@ -62,7 +62,7 @@ public class SearchActionTest { private ProjectMeasuresIndexer projectMeasureIndexer = new ProjectMeasuresIndexer(null, es.client()); private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, projectMeasureIndexer); - private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new AuthorizationTypeSupport(userSession), System2.INSTANCE); + private ProjectMeasuresIndex index = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(userSession), System2.INSTANCE); private WsActionTester ws = new WsActionTester(new SearchAction(index)); @@ -110,7 +110,7 @@ public class SearchActionTest { private void index(ProjectMeasuresDoc... docs) { es.putDocuments(INDEX_TYPE_PROJECT_MEASURES, docs); for (ProjectMeasuresDoc doc : docs) { - PermissionIndexerDao.Dto access = new PermissionIndexerDao.Dto(doc.getId(), Qualifiers.PROJECT); + IndexPermissions access = new IndexPermissions(doc.getId(), Qualifiers.PROJECT); access.allowAnyone(); authorizationIndexerTester.allow(access); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java index d87519555ee..b8f6acf4e8f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/securityreport/ws/ShowActionTest.java @@ -46,8 +46,8 @@ import org.sonar.server.es.StartupIndexer; import org.sonar.server.issue.index.IssueIndex; import org.sonar.server.issue.index.IssueIndexer; import org.sonar.server.issue.index.IssueIteratorFactory; -import org.sonar.server.permission.index.AuthorizationTypeSupport; import org.sonar.server.permission.index.PermissionIndexer; +import org.sonar.server.permission.index.WebAuthorizationTypeSupport; import org.sonar.server.tester.UserSessionRule; import org.sonar.server.ws.WsActionTester; @@ -71,7 +71,7 @@ public class ShowActionTest { private DbClient dbClient = db.getDbClient(); private DbSession session = db.getSession(); - private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new AuthorizationTypeSupport(userSessionRule)); + private IssueIndex issueIndex = new IssueIndex(es.client(), System2.INSTANCE, userSessionRule, new WebAuthorizationTypeSupport(userSessionRule)); private IssueIndexer issueIndexer = new IssueIndexer(es.client(), dbClient, new IssueIteratorFactory(dbClient)); private WsActionTester ws = new WsActionTester(new ShowAction(userSessionRule, TestComponentFinder.from(db), issueIndex, dbClient)); private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);