]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-9551 WS api/issues/search search by application
authorTeryk Bellahsene <teryk.bellahsene@sonarsource.com>
Wed, 26 Jul 2017 12:47:18 +0000 (14:47 +0200)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Mon, 31 Jul 2017 09:27:51 +0000 (11:27 +0200)
server/sonar-server/src/main/java/org/sonar/server/issue/IssueQueryFactory.java
server/sonar-server/src/test/java/org/sonar/server/issue/IssueQueryFactoryTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsMediumTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java [new file with mode: 0644]

index b988c3d65c7dc49d90ec3729bef814cb49f7c9e1..83b525581677ac6831868870e788aa729bff54b5 100644 (file)
@@ -25,14 +25,13 @@ import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 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;
@@ -59,11 +58,13 @@ import org.sonarqube.ws.client.issue.SearchWsRequest;
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.collect.Lists.newArrayList;
 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.uniqueIndex;
 import static org.sonar.server.ws.WsUtils.checkFoundWithOptional;
 import static org.sonar.server.ws.WsUtils.checkRequest;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENTS;
@@ -125,12 +126,7 @@ public class IssueQueryFactory {
       addComponentParameters(builder, dbSession,
         effectiveOnComponentOnly,
         allComponentUuids,
-        request.getProjectUuids(),
-        request.getProjectKeys(),
-        request.getModuleUuids(),
-        request.getDirectories(),
-        request.getFileUuids(),
-        request.getAuthors());
+        request);
 
       builder.createdAfter(buildCreatedAfterFromRequest(dbSession, request, allComponentUuids));
 
@@ -205,12 +201,12 @@ public class IssueQueryFactory {
   }
 
   private boolean mergeDeprecatedComponentParameters(DbSession session, @Nullable Boolean onComponentOnly,
-                                                     @Nullable Collection<String> components,
-                                                     @Nullable Collection<String> componentUuids,
-                                                     @Nullable Collection<String> componentKeys,
-                                                     @Nullable Collection<String> componentRootUuids,
-                                                     @Nullable Collection<String> componentRoots,
-                                                     Set<String> allComponentUuids) {
+    @Nullable Collection<String> components,
+    @Nullable Collection<String> componentUuids,
+    @Nullable Collection<String> componentKeys,
+    @Nullable Collection<String> componentRootUuids,
+    @Nullable Collection<String> componentRoots,
+    Set<String> allComponentUuids) {
     boolean effectiveOnComponentOnly = false;
 
     checkArgument(atMostOneNonNullElement(components, componentUuids, componentKeys, componentRootUuids, componentRoots),
@@ -243,13 +239,9 @@ public class IssueQueryFactory {
   }
 
   private void addComponentParameters(IssueQuery.Builder builder, DbSession session,
-                                      boolean onComponentOnly,
-                                      Collection<String> componentUuids,
-                                      @Nullable Collection<String> projectUuids, @Nullable Collection<String> projectKeys,
-                                      @Nullable Collection<String> moduleUuids,
-                                      @Nullable Collection<String> directories,
-                                      @Nullable Collection<String> fileUuids,
-                                      @Nullable Collection<String> authors) {
+    boolean onComponentOnly,
+    Collection<String> componentUuids,
+    SearchWsRequest request) {
 
     builder.onComponentOnly(onComponentOnly);
     if (onComponentOnly) {
@@ -257,46 +249,48 @@ public class IssueQueryFactory {
       return;
     }
 
-    builder.authors(authors);
+    builder.authors(request.getAuthors());
+    List<String> projectUuids = request.getProjectUuids();
+    List<String> 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) {
       builder.projectUuids(convertComponentKeysToUuids(session, projectKeys));
     }
-    builder.moduleUuids(moduleUuids);
-    builder.directories(directories);
-    builder.fileUuids(fileUuids);
+    builder.moduleUuids(request.getModuleUuids());
+    builder.directories(request.getDirectories());
+    builder.fileUuids(request.getFileUuids());
 
     if (!componentUuids.isEmpty()) {
-      addComponentsBasedOnQualifier(builder, session, componentUuids);
+      addComponentsBasedOnQualifier(builder, session, componentUuids, request);
     }
   }
 
-  private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession session, Collection<String> componentUuids) {
+  private void addComponentsBasedOnQualifier(IssueQuery.Builder builder, DbSession dbSession, Collection<String> componentUuids, SearchWsRequest request) {
     if (componentUuids.isEmpty()) {
       builder.componentUuids(componentUuids);
       return;
     }
 
-    List<ComponentDto> components = dbClient.componentDao().selectByUuids(session, componentUuids);
+    List<ComponentDto> components = dbClient.componentDao().selectByUuids(dbSession, componentUuids);
     if (components.isEmpty()) {
       builder.componentUuids(componentUuids);
       return;
     }
 
     Set<String> qualifiers = components.stream().map(ComponentDto::qualifier).collect(MoreCollectors.toHashSet());
-    if (qualifiers.size() > 1) {
-      throw new IllegalArgumentException("All components must have the same qualifier, found " + Joiner.on(',').join(qualifiers));
-    }
+    checkArgument(qualifiers.size() == 1, "All components must have the same qualifier, found %s", String.join(",", qualifiers));
 
     String qualifier = qualifiers.iterator().next();
     switch (qualifier) {
       case Qualifiers.VIEW:
       case Qualifiers.SUBVIEW:
-      case Qualifiers.APP:
         addViewsOrSubViews(builder, componentUuids);
         break;
+      case Qualifiers.APP:
+        addApplications(builder, dbSession, components, request);
+        break;
       case Qualifiers.PROJECT:
         builder.projectUuids(componentUuids);
         break;
@@ -326,6 +320,32 @@ public class IssueQueryFactory {
     builder.viewUuids(filteredViewUuids);
   }
 
+  private void addApplications(IssueQuery.Builder builder, DbSession dbSession, List<ComponentDto> applications, SearchWsRequest request) {
+    Set<String> authorizedApplicationUuids = applications.stream()
+      .filter(app -> userSession.hasComponentPermission(UserRole.USER, app))
+      .map(ComponentDto::uuid)
+      .collect(MoreCollectors.toSet());
+
+    builder.viewUuids(authorizedApplicationUuids.isEmpty() ? singleton(UNKNOWN) : authorizedApplicationUuids);
+    addCreatedAfterByProjects(builder, dbSession, request, authorizedApplicationUuids);
+  }
+
+  private void addCreatedAfterByProjects(IssueQuery.Builder builder, DbSession dbSession, SearchWsRequest request, Set<String> applicationUuids) {
+    if (request.getSinceLeakPeriod() == null || !request.getSinceLeakPeriod()) {
+      return;
+    }
+
+    Set<String> projectUuids = applicationUuids.stream()
+      .flatMap(app -> dbClient.componentDao().selectProjectsFromView(dbSession, app, app).stream())
+      .collect(MoreCollectors.toSet());
+
+    Map<String, Date> leakByProjects = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids(dbSession, projectUuids)
+      .stream()
+      .filter(s -> s.getPeriodDate() != null)
+      .collect(uniqueIndex(SnapshotDto::getComponentUuid, s -> longToDate(s.getPeriodDate())));
+    builder.createdAfterByProjectUuids(leakByProjects);
+  }
+
   private static void addDirectories(IssueQuery.Builder builder, List<ComponentDto> directories) {
     Collection<String> directoryModuleUuids = new HashSet<>();
     Collection<String> directoryPaths = new HashSet<>();
index 3bc04036e0fe951cc335cb3355d4d1b4796516b2..32dca3502e192348f92fa2207736577ed6ec9474 100644 (file)
@@ -30,6 +30,7 @@ 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.component.SnapshotDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.tester.UserSessionRule;
@@ -37,10 +38,14 @@ import org.sonarqube.ws.client.issue.SearchWsRequest;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.guava.api.Assertions.entry;
 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.newProjectCopy;
 
 public class IssueQueryFactoryTest {
 
@@ -199,6 +204,44 @@ public class IssueQueryFactoryTest {
     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 SearchWsRequest().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 SearchWsRequest()
+      .setComponentUuids(singletonList(application.uuid()))
+      .setSinceLeakPeriod(true)
+    );
+
+    assertThat(result.createdAfterByProjectUuids()).containsOnly(
+        entry(project1.uuid(), new Date(analysis1.getPeriodDate())));
+    assertThat(result.viewUuids()).containsExactlyInAnyOrder(application.uuid());
+  }
+
   @Test
   public void return_empty_results_if_not_allowed_to_search_for_subview() {
     ComponentDto view = db.components().insertView();
index 4e5ddce0cd366077b533df0d4f45ae9d0def8ddd..52df082d7bd5dc2614790aeed321e89a33ee7545 100644 (file)
@@ -67,6 +67,10 @@ import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SEARCH;
 import static org.sonarqube.ws.client.issue.IssuesWsParameters.CONTROLLER_ISSUES;
 
+/**
+ * @deprecated use {@link SearchActionComponentsTest} instead
+ */
+@Deprecated
 public class SearchActionComponentsMediumTest {
 
   @ClassRule
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
new file mode 100644 (file)
index 0000000..7fc36cb
--- /dev/null
@@ -0,0 +1,260 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 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.ws;
+
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.utils.Durations;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.IssueQueryFactory;
+import org.sonar.server.issue.index.IssueIndex;
+import org.sonar.server.issue.index.IssueIndexDefinition;
+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.tester.UserSessionRule;
+import org.sonar.server.view.index.ViewIndexDefinition;
+import org.sonar.server.view.index.ViewIndexer;
+import org.sonar.server.ws.WsActionTester;
+import org.sonar.server.ws.WsResponseCommonFormat;
+import org.sonarqube.ws.Issues.Issue;
+import org.sonarqube.ws.Issues.SearchWsResponse;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.utils.DateUtils.addDays;
+import static org.sonar.db.component.ComponentTesting.newProjectCopy;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_COMPONENT_KEYS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_KEYS;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_SINCE_LEAK_PERIOD;
+
+public class SearchActionComponentsTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public EsTester es = new EsTester(
+    new IssueIndexDefinition(new MapSettings().asConfig()),
+    new ViewIndexDefinition(new MapSettings().asConfig()));
+
+  private IssueIndex index = new IssueIndex(es.client(), System2.INSTANCE, userSession, new AuthorizationTypeSupport(userSession));
+  private IssueIndexer issueIndexer = new IssueIndexer(es.client(), db.getDbClient(), new IssueIteratorFactory(db.getDbClient()));
+  private ViewIndexer viewIndexer = new ViewIndexer(db.getDbClient(), es.client());
+  private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
+  private IssueQueryFactory issueQueryFactory = new IssueQueryFactory(db.getDbClient(), System2.INSTANCE, userSession);
+  private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, db.getDbClient(), null, null);
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(new Languages()), new Languages(),
+    new AvatarResolverImpl());
+
+  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, index, issueQueryFactory, searchResponseLoader, searchResponseFormat));
+
+  @Test
+  public void search_by_application_key() throws Exception {
+    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));
+    IssueDto issue1 = db.issues().insertIssue(i -> i.setProject(project1));
+    IssueDto issue2 = db.issues().insertIssue(i -> i.setProject(project2));
+    userSession.registerComponents(application, project1, project2);
+    permissionIndexer.allowOnlyAnyone(project1);
+    permissionIndexer.allowOnlyAnyone(project2);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(issue1.getKey(), issue2.getKey());
+  }
+
+  @Test
+  public void ignore_application_without_browse_permission() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto application = db.components().insertApplication(db.getDefaultOrganization());
+    db.components().insertComponents(newProjectCopy("PC1", project, application));
+    db.issues().insertIssue(i -> i.setProject(project));
+    userSession.registerComponents(project);
+    permissionIndexer.allowOnlyAnyone(project);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).isEmpty();
+  }
+
+  @Test
+  public void search_application_without_projects() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto application = db.components().insertApplication(db.getDefaultOrganization());
+    db.issues().insertIssue(i -> i.setProject(project));
+    userSession.registerComponents(application, project);
+    permissionIndexer.allowOnlyAnyone(project);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).isEmpty();
+  }
+
+  @Test
+  public void search_by_application_and_by_leak() throws Exception {
+    Date now = new Date();
+    ComponentDto application = db.components().insertApplication(db.getDefaultOrganization());
+    // Project 1
+    ComponentDto project1 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project1, s -> s.setPeriodDate(addDays(now, -14).getTime()));
+    db.components().insertComponents(newProjectCopy("PC1", project1, application));
+    IssueDto project1Issue1 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -10)));
+    IssueDto project1Issue2 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -20)));
+    // Project 2
+    ComponentDto project2 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project2, s -> s.setPeriodDate(addDays(now, -25).getTime()));
+    db.components().insertComponents(newProjectCopy("PC2", project2, application));
+    IssueDto project2Issue1 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -15)));
+    IssueDto project2Issue2 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -30)));
+    // Permissions and index
+    userSession.registerComponents(application, project1, project2);
+    permissionIndexer.allowOnlyAnyone(project1);
+    permissionIndexer.allowOnlyAnyone(project2);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .setParam(PARAM_SINCE_LEAK_PERIOD, "true")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(project1Issue1.getKey(), project2Issue1.getKey())
+      .doesNotContain(project1Issue2.getKey(), project2Issue2.getKey());
+  }
+
+  @Test
+  public void search_by_application_and_project() throws Exception {
+    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));
+    IssueDto issue1 = db.issues().insertIssue(i -> i.setProject(project1));
+    IssueDto issue2 = db.issues().insertIssue(i -> i.setProject(project2));
+    userSession.registerComponents(application, project1, project2);
+    permissionIndexer.allowOnlyAnyone(project1);
+    permissionIndexer.allowOnlyAnyone(project2);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .setParam(PARAM_PROJECT_KEYS, project1.getKey())
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(issue1.getKey())
+      .doesNotContain(issue2.getKey());
+  }
+
+  @Test
+  public void search_by_application_and_project_and_leak() throws Exception {
+    Date now = new Date();
+    ComponentDto application = db.components().insertApplication(db.getDefaultOrganization());
+    // Project 1
+    ComponentDto project1 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project1, s -> s.setPeriodDate(addDays(now, -14).getTime()));
+    db.components().insertComponents(newProjectCopy("PC1", project1, application));
+    IssueDto project1Issue1 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -10)));
+    IssueDto project1Issue2 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -20)));
+    // Project 2
+    ComponentDto project2 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project2, s -> s.setPeriodDate(addDays(now, -25).getTime()));
+    db.components().insertComponents(newProjectCopy("PC2", project2, application));
+    IssueDto project2Issue1 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -15)));
+    IssueDto project2Issue2 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -30)));
+    // Permissions and index
+    userSession.registerComponents(application, project1, project2);
+    permissionIndexer.allowOnlyAnyone(project1);
+    permissionIndexer.allowOnlyAnyone(project2);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .setParam(PARAM_PROJECT_KEYS, project1.getKey())
+      .setParam(PARAM_SINCE_LEAK_PERIOD, "true")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(project1Issue1.getKey())
+      .doesNotContain(project1Issue2.getKey(), project2Issue1.getKey(), project2Issue2.getKey());
+  }
+
+  @Test
+  public void search_by_application_and_by_leak_when_one_project_has_no_leak() throws Exception {
+    Date now = new Date();
+    ComponentDto application = db.components().insertApplication(db.getDefaultOrganization());
+    // Project 1
+    ComponentDto project1 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project1, s -> s.setPeriodDate(addDays(now, -14).getTime()));
+    db.components().insertComponents(newProjectCopy("PC1", project1, application));
+    IssueDto project1Issue1 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -10)));
+    IssueDto project1Issue2 = db.issues().insertIssue(i -> i.setProject(project1).setIssueCreationDate(addDays(now, -20)));
+    // Project 2, without leak => no issue form it should be returned
+    ComponentDto project2 = db.components().insertPublicProject();
+    db.components().insertSnapshot(project2, s -> s.setPeriodDate(null));
+    db.components().insertComponents(newProjectCopy("PC2", project2, application));
+    IssueDto project2Issue1 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -15)));
+    IssueDto project2Issue2 = db.issues().insertIssue(i -> i.setProject(project2).setIssueCreationDate(addDays(now, -30)));
+    // Permissions and index
+    userSession.registerComponents(application, project1, project2);
+    permissionIndexer.allowOnlyAnyone(project1);
+    permissionIndexer.allowOnlyAnyone(project2);
+    indexIssuesAndViews();
+
+    SearchWsResponse result = ws.newRequest()
+      .setParam(PARAM_COMPONENT_KEYS, application.getKey())
+      .setParam(PARAM_SINCE_LEAK_PERIOD, "true")
+      .executeProtobuf(SearchWsResponse.class);
+
+    assertThat(result.getIssuesList()).extracting(Issue::getKey)
+      .containsExactlyInAnyOrder(project1Issue1.getKey())
+      .doesNotContain(project1Issue2.getKey(), project2Issue1.getKey(), project2Issue2.getKey());
+  }
+
+  private void indexIssuesAndViews() {
+    issueIndexer.indexOnStartup(null);
+    viewIndexer.indexOnStartup(null);
+  }
+}