From e461d574c67ae1b3433e6314a1c938ef4660a2d5 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Fri, 5 May 2017 16:42:11 +0200 Subject: [PATCH] SONAR-9186 make "query" optional for api/components/suggestions --- .../server/component/index/ComponentHit.java | 7 +- .../component/ws/SuggestionsAction.java | 207 +++++++++++----- .../component/ws/SuggestionsActionTest.java | 221 +++++++++++++++++- 3 files changed, 359 insertions(+), 76 deletions(-) diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentHit.java b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentHit.java index 891edc79791..4da8d6ef50e 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentHit.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/index/ComponentHit.java @@ -34,7 +34,12 @@ public class ComponentHit { private final String uuid; private final Optional highlightedText; - private ComponentHit(SearchHit hit) { + public ComponentHit(String uuid) { + this.uuid = uuid; + highlightedText = Optional.empty(); + } + + public ComponentHit(SearchHit hit) { this.uuid = hit.getId(); this.highlightedText = getHighlightedText(hit); } diff --git a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java index 8250205dc16..5f18b74058d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/component/ws/SuggestionsAction.java @@ -19,14 +19,20 @@ */ package org.sonar.server.component.ws; +import com.google.common.collect.ListMultimap; import com.google.common.html.HtmlEscapers; import com.google.common.io.Resources; import java.util.Collection; +import java.util.Comparator; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Nullable; +import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; import org.sonar.api.server.ws.WebService; @@ -43,6 +49,7 @@ import org.sonar.server.component.index.ComponentIndexQuery; import org.sonar.server.component.index.ComponentIndexResults; import org.sonar.server.es.textsearch.ComponentTextSearchFeature; import org.sonar.server.favorite.FavoriteFinder; +import org.sonar.server.user.UserSession; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Category; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; @@ -53,6 +60,7 @@ import static java.util.Arrays.stream; import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; +import static org.sonar.api.web.UserRole.USER; import static org.sonar.core.util.stream.MoreCollectors.toList; import static org.sonar.core.util.stream.MoreCollectors.toSet; import static org.sonar.server.es.DefaultIndexSettings.MINIMUM_NGRAM_LENGTH; @@ -67,19 +75,21 @@ public class SuggestionsAction implements ComponentsWsAction { static final String PARAM_MORE = "more"; static final String PARAM_RECENTLY_BROWSED = "recentlyBrowsed"; static final String SHORT_INPUT_WARNING = "short_input"; - private static final long MAXIMUM_RECENTLY_BROWSED = 50; + private static final int MAXIMUM_RECENTLY_BROWSED = 50; static final int EXTENDED_LIMIT = 20; private final ComponentIndex index; private final FavoriteFinder favoriteFinder; + private final UserSession userSession; private DbClient dbClient; - public SuggestionsAction(DbClient dbClient, ComponentIndex index, FavoriteFinder favoriteFinder) { + public SuggestionsAction(DbClient dbClient, ComponentIndex index, FavoriteFinder favoriteFinder, UserSession userSession) { this.dbClient = dbClient; this.index = index; this.favoriteFinder = favoriteFinder; + this.userSession = userSession; } @Override @@ -97,10 +107,11 @@ public class SuggestionsAction implements ComponentsWsAction { .setSince("4.2") .setInternal(true) .setHandler(this) - .setResponseExample(Resources.getResource(this.getClass(), "components-example-suggestions.json")); + .setResponseExample(Resources.getResource(this.getClass(), "components-example-suggestions.json")) + .setChangelog(new Change("6.4", "Parameter 's' is optional")); action.createParam(PARAM_QUERY) - .setRequired(true) + .setRequired(false) .setDescription("Search query with a minimum of two characters. Can contain several search tokens, separated by spaces. " + "Search tokens with only one character will be ignored.") .setExampleValue("sonar"); @@ -115,99 +126,167 @@ public class SuggestionsAction implements ComponentsWsAction { + " items will be used. Order is not taken into account.") .setSince("6.4") .setExampleValue("org.sonarsource:sonarqube,some.other:project") - .setRequired(false); + .setRequired(false) + .setMaxValuesAllowed(MAXIMUM_RECENTLY_BROWSED); } @Override public void handle(Request wsRequest, Response wsResponse) throws Exception { String query = wsRequest.param(PARAM_QUERY); String more = wsRequest.param(PARAM_MORE); + Set recentlyBrowsedKeys = getRecentlyBrowsedKeys(wsRequest); + List qualifiers = getQualifiers(more); + SuggestionsWsResponse searchWsResponse = loadSuggestions(query, more, recentlyBrowsedKeys, qualifiers); + writeProtobuf(searchWsResponse, wsRequest, wsResponse); + } + + private static Set getRecentlyBrowsedKeys(Request wsRequest) { List recentlyBrowsedParam = wsRequest.paramAsStrings(PARAM_RECENTLY_BROWSED); - Set recentlyBrowsedKeys; if (recentlyBrowsedParam == null) { - recentlyBrowsedKeys = emptySet(); - } else { - recentlyBrowsedKeys = recentlyBrowsedParam.stream().limit(MAXIMUM_RECENTLY_BROWSED).collect(Collectors.toSet()); + return emptySet(); + } + return new HashSet<>(recentlyBrowsedParam); + } + + private SuggestionsWsResponse loadSuggestions(@Nullable String query, String more, Set recentlyBrowsedKeys, List qualifiers) { + if (query == null) { + return loadSuggestionsWithoutSearch(more, recentlyBrowsedKeys, qualifiers); } - Set favoriteKeys = favoriteFinder.list().stream().map(ComponentDto::getKey).collect(Collectors.toSet()); + return loadSuggestionsWithSearch(query, more, recentlyBrowsedKeys, qualifiers); + } + + /** + * we are generating suggestions, by using (1) favorites and (2) recently browsed components (without searchin in Elasticsearch) + */ + private SuggestionsWsResponse loadSuggestionsWithoutSearch(@Nullable String more, Set recentlyBrowsedKeys, List qualifiers) { + List favoriteDtos = favoriteFinder.list(); + if (favoriteDtos.isEmpty() && recentlyBrowsedKeys.isEmpty()) { + return newBuilder().build(); + } + try (DbSession dbSession = dbClient.openSession(false)) { + Set componentDtos = new HashSet<>(favoriteDtos); + if (!recentlyBrowsedKeys.isEmpty()) { + componentDtos.addAll(dbClient.componentDao().selectByKeys(dbSession, recentlyBrowsedKeys)); + } + ListMultimap componentsPerQualifier = componentDtos.stream() + .filter(c -> userSession.hasComponentPermission(USER, c)) + .collect(MoreCollectors.index(ComponentDto::qualifier)); + if (componentsPerQualifier.isEmpty()) { + return newBuilder().build(); + } + + Set favoriteUuids = favoriteDtos.stream().map(ComponentDto::uuid).collect(MoreCollectors.toSet(favoriteDtos.size())); + Comparator favoriteComparator = Comparator.comparing(c -> favoriteUuids.contains(c.uuid()) ? -1 : +1); + Comparator comparator = favoriteComparator.thenComparing(ComponentDto::name); + + int limit = more == null ? ComponentIndexQuery.DEFAULT_LIMIT : EXTENDED_LIMIT; + ComponentIndexResults componentsPerQualifiers = ComponentIndexResults.newBuilder().setQualifiers( + qualifiers.stream().map(q -> { + List hits = componentsPerQualifier.get(q) + .stream() + .sorted(comparator) + .limit(limit) + .map(ComponentDto::uuid) + .map(ComponentHit::new) + .collect(MoreCollectors.toList(limit)); + int totalHits = componentsPerQualifier.size(); + return new ComponentHitsPerQualifier(q, hits, totalHits); + })).build(); + return buildResponse(recentlyBrowsedKeys, favoriteUuids, componentsPerQualifiers, dbSession, componentDtos.stream()).build(); + } + } + private SuggestionsWsResponse loadSuggestionsWithSearch(String query, @Nullable String more, Set recentlyBrowsedKeys, List qualifiers) { + List favorites = favoriteFinder.list(); + Set favoriteKeys = favorites.stream().map(ComponentDto::getKey).collect(MoreCollectors.toSet(favorites.size())); ComponentIndexQuery.Builder queryBuilder = ComponentIndexQuery.builder() .setQuery(query) .setRecentlyBrowsedKeys(recentlyBrowsedKeys) - .setFavoriteKeys(favoriteKeys); - - ComponentIndexResults componentsPerQualifiers = getComponentsPerQualifiers(more, queryBuilder); - String warning = getWarning(query); - - SuggestionsWsResponse searchWsResponse = toResponse(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, warning); - writeProtobuf(searchWsResponse, wsRequest, wsResponse); + .setFavoriteKeys(favoriteKeys) + .setQualifiers(qualifiers); + if (more != null) { + queryBuilder.setLimit(EXTENDED_LIMIT); + } + ComponentIndexResults componentsPerQualifiers = searchInIndex(queryBuilder.build()); + if (componentsPerQualifiers.isEmpty()) { + return newBuilder().build(); + } + try (DbSession dbSession = dbClient.openSession(false)) { + Set componentUuids = componentsPerQualifiers.getQualifiers() + .map(ComponentHitsPerQualifier::getHits) + .flatMap(Collection::stream) + .map(ComponentHit::getUuid) + .collect(toSet()); + Stream componentDtoStream = dbClient.componentDao().selectByUuids(dbSession, componentUuids).stream(); + Set favoriteUuids = favorites.stream().map(ComponentDto::uuid).collect(MoreCollectors.toSet(favorites.size())); + SuggestionsWsResponse.Builder searchWsResponse = buildResponse(recentlyBrowsedKeys, favoriteUuids, componentsPerQualifiers, dbSession, componentDtoStream); + getWarning(query).ifPresent(searchWsResponse::setWarning); + return searchWsResponse.build(); + } } - private static String getWarning(String query) { + private static Optional getWarning(String query) { List tokens = ComponentTextSearchFeature.split(query).collect(Collectors.toList()); if (tokens.stream().anyMatch(token -> token.length() < MINIMUM_NGRAM_LENGTH)) { - return SHORT_INPUT_WARNING; + return Optional.of(SHORT_INPUT_WARNING); } - return null; + return Optional.empty(); } - private ComponentIndexResults getComponentsPerQualifiers(@Nullable String more, ComponentIndexQuery.Builder queryBuilder) { - List qualifiers; + private static List getQualifiers(@Nullable String more) { if (more == null) { - qualifiers = stream(SuggestionCategory.values()).map(SuggestionCategory::getQualifier).collect(Collectors.toList()); - } else { - qualifiers = singletonList(SuggestionCategory.getByName(more).getQualifier()); - queryBuilder.setLimit(EXTENDED_LIMIT); + return stream(SuggestionCategory.values()).map(SuggestionCategory::getQualifier).collect(Collectors.toList()); } - queryBuilder.setQualifiers(qualifiers); - return searchInIndex(queryBuilder.build()); + return singletonList(SuggestionCategory.getByName(more).getQualifier()); + } + + private SuggestionsWsResponse.Builder buildResponse(Set recentlyBrowsedKeys, Set favoriteUuids, ComponentIndexResults componentsPerQualifiers, DbSession dbSession, + Stream stream) { + Map componentsByUuids = stream + .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); + Map organizationsByUuids = loadOrganizations(dbSession, componentsByUuids.values()); + Map projectsByUuids = loadProjects(dbSession, componentsByUuids.values()); + return toResponse(componentsPerQualifiers, recentlyBrowsedKeys, favoriteUuids, organizationsByUuids, componentsByUuids, projectsByUuids); + } + + private Map loadProjects(DbSession dbSession, Collection components) { + Set projectUuids = components.stream() + .filter(c -> !c.projectUuid().equals(c.uuid())) + .map(ComponentDto::projectUuid) + .collect(MoreCollectors.toSet()); + return dbClient.componentDao().selectByUuids(dbSession, projectUuids).stream() + .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); + } + + private Map loadOrganizations(DbSession dbSession, Collection components) { + Set organizationUuids = components.stream() + .map(ComponentDto::getOrganizationUuid) + .collect(MoreCollectors.toSet()); + return dbClient.organizationDao().selectByUuids(dbSession, organizationUuids).stream() + .collect(MoreCollectors.uniqueIndex(OrganizationDto::getUuid)); } private ComponentIndexResults searchInIndex(ComponentIndexQuery componentIndexQuery) { return index.search(componentIndexQuery); } - private SuggestionsWsResponse toResponse(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteKeys, @Nullable String warning) { - SuggestionsWsResponse.Builder builder = newBuilder(); - if (!componentsPerQualifiers.isEmpty()) { - Map organizationsByUuids; - Map componentsByUuids; - Map projectsByUuids; - try (DbSession dbSession = dbClient.openSession(false)) { - Set componentUuids = componentsPerQualifiers.getQualifiers() - .map(ComponentHitsPerQualifier::getHits) - .flatMap(Collection::stream) - .map(ComponentHit::getUuid) - .collect(toSet()); - componentsByUuids = dbClient.componentDao().selectByUuids(dbSession, componentUuids).stream() - .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); - Set organizationUuids = componentsByUuids.values().stream() - .map(ComponentDto::getOrganizationUuid) - .collect(toSet()); - organizationsByUuids = dbClient.organizationDao().selectByUuids(dbSession, organizationUuids).stream() - .collect(MoreCollectors.uniqueIndex(OrganizationDto::getUuid)); - Set projectUuids = componentsByUuids.values().stream() - .filter(c -> !c.projectUuid().equals(c.uuid())) - .map(ComponentDto::projectUuid) - .collect(toSet()); - projectsByUuids = dbClient.componentDao().selectByUuids(dbSession, projectUuids).stream() - .collect(MoreCollectors.uniqueIndex(ComponentDto::uuid)); - } - builder - .addAllResults(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationsByUuids, projectsByUuids)) - .addAllOrganizations(toOrganizations(organizationsByUuids)) - .addAllProjects(toProjects(projectsByUuids)); + private static SuggestionsWsResponse.Builder toResponse(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteUuids, + Map organizationsByUuids, Map componentsByUuids, Map projectsByUuids) { + if (componentsPerQualifiers.isEmpty()) { + return newBuilder(); } - ofNullable(warning).ifPresent(builder::setWarning); - return builder.build(); + return newBuilder() + .addAllResults(toCategories(componentsPerQualifiers, recentlyBrowsedKeys, favoriteUuids, componentsByUuids, organizationsByUuids, projectsByUuids)) + .addAllOrganizations(toOrganizations(organizationsByUuids)) + .addAllProjects(toProjects(projectsByUuids)); } - private static List toCategories(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteKeys, + private static List toCategories(ComponentIndexResults componentsPerQualifiers, Set recentlyBrowsedKeys, Set favoriteUuids, Map componentsByUuids, Map organizationByUuids, Map projectsByUuids) { return componentsPerQualifiers.getQualifiers().map(qualifier -> { List suggestions = qualifier.getHits().stream() - .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, favoriteKeys, componentsByUuids, organizationByUuids, projectsByUuids)) + .map(hit -> toSuggestion(hit, recentlyBrowsedKeys, favoriteUuids, componentsByUuids, organizationByUuids, projectsByUuids)) .collect(toList()); return Category.newBuilder() @@ -218,7 +297,7 @@ public class SuggestionsAction implements ComponentsWsAction { }).collect(toList()); } - private static Suggestion toSuggestion(ComponentHit hit, Set recentlyBrowsedKeys, Set favoriteKeys, Map componentsByUuids, + private static Suggestion toSuggestion(ComponentHit hit, Set recentlyBrowsedKeys, Set favoriteUuids, Map componentsByUuids, Map organizationByUuids, Map projectsByUuids) { ComponentDto result = componentsByUuids.get(hit.getUuid()); String organizationKey = organizationByUuids.get(result.getOrganizationUuid()).getKey(); @@ -231,7 +310,7 @@ public class SuggestionsAction implements ComponentsWsAction { .setName(result.longName()) .setMatch(hit.getHighlightedText().orElse(HtmlEscapers.htmlEscaper().escape(result.longName()))) .setIsRecentlyBrowsed(recentlyBrowsedKeys.contains(result.getKey())) - .setIsFavorite(favoriteKeys.contains(result.getKey())) + .setIsFavorite(favoriteUuids.contains(result.uuid())) .build(); } 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 c5ad2411795..ae89d6f6c5e 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 @@ -29,6 +29,7 @@ import org.junit.Rule; import org.junit.Test; import org.sonar.api.config.MapSettings; import org.sonar.api.resources.Qualifiers; +import org.sonar.api.server.ws.Change; import org.sonar.api.server.ws.WebService; import org.sonar.api.utils.System2; import org.sonar.db.DbTester; @@ -49,6 +50,7 @@ import org.sonarqube.ws.WsComponents.SuggestionsWsResponse; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Project; import org.sonarqube.ws.WsComponents.SuggestionsWsResponse.Suggestion; +import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.joining; @@ -58,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.sonar.api.web.UserRole.USER; import static org.sonar.db.component.ComponentTesting.newModuleDto; import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; import static org.sonar.server.component.index.ComponentIndexQuery.DEFAULT_LIMIT; @@ -81,10 +84,10 @@ 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)); - private SuggestionsAction underTest = new SuggestionsAction(db.getDbClient(), index, favoriteFinder); + private SuggestionsAction underTest = new SuggestionsAction(db.getDbClient(), index, favoriteFinder, userSessionRule); private OrganizationDto organization; private PermissionIndexerTester authorizationIndexerTester = new PermissionIndexerTester(es, componentIndexer); - private WsActionTester actionTester = new WsActionTester(underTest); + private WsActionTester ws = new WsActionTester(underTest); @Before public void setUp() { @@ -93,7 +96,7 @@ public class SuggestionsActionTest { @Test public void define_suggestions_action() { - WebService.Action action = actionTester.getDef(); + WebService.Action action = ws.getDef(); assertThat(action).isNotNull(); assertThat(action.isInternal()).isTrue(); assertThat(action.isPost()).isFalse(); @@ -103,12 +106,192 @@ public class SuggestionsActionTest { PARAM_MORE, PARAM_QUERY, PARAM_RECENTLY_BROWSED); + assertThat(action.changelog()).extracting(Change::getVersion, Change::getDescription).containsExactlyInAnyOrder( + tuple("6.4", "Parameter 's' is optional")); WebService.Param recentlyBrowsed = action.param(PARAM_RECENTLY_BROWSED); assertThat(recentlyBrowsed.since()).isEqualTo("6.4"); assertThat(recentlyBrowsed.exampleValue()).isNotEmpty(); assertThat(recentlyBrowsed.description()).isNotEmpty(); assertThat(recentlyBrowsed.isRequired()).isFalse(); + + WebService.Param query = action.param(PARAM_QUERY); + assertThat(query.exampleValue()).isNotEmpty(); + assertThat(query.description()).isNotEmpty(); + assertThat(query.isRequired()).isFalse(); + } + + @Test + public void suggestions_without_query_should_contain_recently_browsed() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + + componentIndexer.indexOnStartup(null); + userSessionRule.addProjectPermission(USER, project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RECENTLY_BROWSED, project.getKey()) + .executeProtobuf(SuggestionsWsResponse.class); + + // assert match in qualifier "TRK" + assertThat(response.getResultsList()) + .filteredOn(q -> q.getItemsCount() > 0) + .extracting(Category::getQ) + .containsExactly(Qualifiers.PROJECT); + + // assert correct id to be found + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .extracting(Suggestion::getKey, Suggestion::getIsRecentlyBrowsed) + .containsExactly(tuple(project.getKey(), true)); + } + + @Test + public void suggestions_without_query_should_not_contain_recently_browsed_without_permission() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + + componentIndexer.indexOnStartup(null); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RECENTLY_BROWSED, project.getKey()) + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .isEmpty(); + } + + @Test + public void suggestions_without_query_should_contain_favorites() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + doReturn(singletonList(project)).when(favoriteFinder).list(); + + componentIndexer.indexOnStartup(null); + userSessionRule.addProjectPermission(USER, project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .executeProtobuf(SuggestionsWsResponse.class); + + // assert match in qualifier "TRK" + assertThat(response.getResultsList()) + .filteredOn(q -> q.getItemsCount() > 0) + .extracting(Category::getQ) + .containsExactly(Qualifiers.PROJECT); + + // assert correct id to be found + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .extracting(Suggestion::getKey, Suggestion::getIsFavorite) + .containsExactly(tuple(project.getKey(), true)); + } + + @Test + public void suggestions_without_query_should_not_contain_favorites_without_permission() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + doReturn(singletonList(project)).when(favoriteFinder).list(); + + componentIndexer.indexOnStartup(null); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .isEmpty(); + } + + @Test + public void suggestions_without_query_should_contain_recently_browsed_favorites() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + doReturn(singletonList(project)).when(favoriteFinder).list(); + + componentIndexer.indexOnStartup(null); + userSessionRule.addProjectPermission(USER, project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RECENTLY_BROWSED, project.key()) + .executeProtobuf(SuggestionsWsResponse.class); + + // assert match in qualifier "TRK" + assertThat(response.getResultsList()) + .filteredOn(q -> q.getItemsCount() > 0) + .extracting(Category::getQ) + .containsExactly(Qualifiers.PROJECT); + + // assert correct id to be found + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .extracting(Suggestion::getKey, Suggestion::getIsFavorite, Suggestion::getIsRecentlyBrowsed) + .containsExactly(tuple(project.getKey(), true, true)); + } + + @Test + public void suggestions_without_query_should_not_contain_matches_that_are_neither_favorites_nor_recently_browsed() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + + componentIndexer.indexOnStartup(null); + userSessionRule.addProjectPermission(USER, project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .executeProtobuf(SuggestionsWsResponse.class); + + // assert match in qualifier "TRK" + assertThat(response.getResultsList()) + .filteredOn(q -> q.getItemsCount() > 0) + .extracting(Category::getQ) + .isEmpty(); + } + + @Test + public void suggestions_without_query_should_order_results() throws Exception { + ComponentDto project1 = db.components().insertComponent(newPrivateProjectDto(organization).setName("Alpha").setLongName("Alpha")); + ComponentDto project2 = db.components().insertComponent(newPrivateProjectDto(organization).setName("Bravo").setLongName("Bravo")); + ComponentDto project3 = db.components().insertComponent(newPrivateProjectDto(organization).setName("Charlie").setLongName("Charlie")); + ComponentDto project4 = db.components().insertComponent(newPrivateProjectDto(organization).setName("Delta").setLongName("Delta")); + doReturn(asList(project4, project2)).when(favoriteFinder).list(); + + componentIndexer.indexOnStartup(null); + userSessionRule.addProjectPermission(USER, project1); + userSessionRule.addProjectPermission(USER, project2); + userSessionRule.addProjectPermission(USER, project3); + userSessionRule.addProjectPermission(USER, project4); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RECENTLY_BROWSED, Stream.of(project3, project1).map(ComponentDto::getKey).collect(joining(","))) + .executeProtobuf(SuggestionsWsResponse.class); + + // assert order of keys + assertThat(response.getResultsList()) + .flatExtracting(Category::getItemsList) + .extracting(Suggestion::getName, Suggestion::getIsFavorite, Suggestion::getIsRecentlyBrowsed) + .containsExactly( + tuple("Bravo", true, false), + tuple("Delta", true, false), + tuple("Alpha", false, true), + tuple("Charlie", false, true) + ); + } + + @Test + public void suggestions_without_query_should_return_empty_qualifiers() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); + userSessionRule.addProjectPermission(USER, project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_RECENTLY_BROWSED, project.key()) + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getResultsList()) + .extracting(Category::getQ, Category::getItemsCount) + .containsExactlyInAnyOrder(tuple("VW", 0), tuple("SVW", 0), tuple("TRK", 1), tuple("BRC", 0), tuple("FIL", 0), tuple("UTS", 0)); } @Test @@ -118,7 +301,7 @@ public class SuggestionsActionTest { componentIndexer.indexOnStartup(null); authorizationIndexerTester.allowOnlyAnyone(project); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, project.getKey()) .executeProtobuf(SuggestionsWsResponse.class); @@ -143,7 +326,7 @@ public class SuggestionsActionTest { componentIndexer.indexOnStartup(null); authorizationIndexerTester.allowOnlyAnyone(project); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "S o") .executeProtobuf(SuggestionsWsResponse.class); @@ -154,7 +337,7 @@ public class SuggestionsActionTest { @Test public void should_warn_about_short_inputs() throws Exception { - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "validLongToken x") .executeProtobuf(SuggestionsWsResponse.class); @@ -175,7 +358,7 @@ public class SuggestionsActionTest { componentIndexer.indexProject(project2.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); authorizationIndexerTester.allowOnlyAnyone(project2); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "Project") .executeProtobuf(SuggestionsWsResponse.class); @@ -195,7 +378,7 @@ public class SuggestionsActionTest { componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); authorizationIndexerTester.allowOnlyAnyone(project); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "Module") .executeProtobuf(SuggestionsWsResponse.class); @@ -221,7 +404,7 @@ public class SuggestionsActionTest { componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); authorizationIndexerTester.allowOnlyAnyone(project); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "Module") .setParam(PARAM_RECENTLY_BROWSED, Stream.of(module1.getKey()).collect(joining(","))) @@ -245,7 +428,7 @@ public class SuggestionsActionTest { componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); authorizationIndexerTester.allowOnlyAnyone(project); - SuggestionsWsResponse response = actionTester.newRequest() + SuggestionsWsResponse response = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, "Module") .executeProtobuf(SuggestionsWsResponse.class); @@ -256,6 +439,22 @@ public class SuggestionsActionTest { .containsExactly(tuple(favorite.getKey(), true), tuple(nonFavorite.getKey(), false)); } + @Test + public void should_return_empty_qualifiers() throws Exception { + ComponentDto project = db.components().insertComponent(newPrivateProjectDto(organization)); + componentIndexer.indexProject(project.projectUuid(), ProjectIndexer.Cause.PROJECT_CREATION); + authorizationIndexerTester.allowOnlyAnyone(project); + + SuggestionsWsResponse response = ws.newRequest() + .setMethod("POST") + .setParam(PARAM_QUERY, project.name()) + .executeProtobuf(SuggestionsWsResponse.class); + + assertThat(response.getResultsList()) + .extracting(Category::getQ, Category::getItemsCount) + .containsExactlyInAnyOrder(tuple("VW", 0), tuple("SVW", 0), tuple("TRK", 1), tuple("BRC", 0), tuple("FIL", 0), tuple("UTS", 0)); + } + @Test public void should_propose_to_show_more_results_if_7_projects_are_found() throws Exception { check_proposal_to_show_more_results(7, DEFAULT_LIMIT, 1L, null); @@ -286,7 +485,7 @@ public class SuggestionsActionTest { componentIndexer.indexOnStartup(null); projects.forEach(authorizationIndexerTester::allowOnlyAnyone); - TestRequest request = actionTester.newRequest() + TestRequest request = ws.newRequest() .setMethod("POST") .setParam(PARAM_QUERY, namePrefix); ofNullable(more).ifPresent(c -> request.setParam(PARAM_MORE, c.getName())); -- 2.39.5