]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10864 Author information only available to members (#552)
authorEric Hartmann <hartmann.eric@gmail.Com>
Wed, 8 Aug 2018 05:29:27 +0000 (06:29 +0100)
committerSonarTech <sonartech@sonarsource.com>
Wed, 8 Aug 2018 18:21:21 +0000 (20:21 +0200)
39 files changed:
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/DbVersion73Test.java
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchAction.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseData.java
server/sonar-server/src/main/java/org/sonar/server/issue/ws/SearchResponseFormat.java
server/sonar-server/src/main/java/org/sonar/server/source/ws/LinesAction.java
server/sonar-server/src/main/java/org/sonar/server/user/AbstractUserSession.java
server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java
server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java
server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionComponentsTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTest.java
server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/source/ws/LinesActionTest.java
server/sonar-server/src/test/java/org/sonar/server/tester/AbstractMockUserSession.java
server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java
server/sonar-server/src/test/java/org/sonar/server/user/TestUserSessionFactory.java
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json [new file with mode: 0644]
server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json [new file with mode: 0644]
server/sonar-web/src/main/js/apps/explore/ExploreIssues.tsx
server/sonar-web/src/main/js/apps/issues/components/App.tsx
server/sonar-web/src/main/js/apps/issues/components/AppContainer.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
server/sonar-web/src/main/js/apps/issues/utils.ts
server/sonar-web/src/main/js/components/SourceViewer/components/LineSCM.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/SCMPopup.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/LineSCM-test.tsx
server/sonar-web/src/main/js/components/SourceViewer/components/__tests__/__snapshots__/LineSCM-test.tsx.snap

index b27bcb0117958cb95286f5ff8262ba54fb696c0d..e62fc5698f819530ccfd36e1f67b3f2ffb4c6507 100644 (file)
@@ -25,6 +25,7 @@ CREATE TABLE "ORGANIZATION_MEMBERS" (
 
   CONSTRAINT "PK_ORGANIZATION_MEMBERS" PRIMARY KEY ("ORGANIZATION_UUID", "USER_ID")
 );
+CREATE INDEX "IX_ORG_MEMBERS_ON_USER_ID" ON "ORGANIZATION_MEMBERS" ("USER_ID");
 
 CREATE TABLE "GROUPS_USERS" (
   "USER_ID" INTEGER,
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembers.java
new file mode 100644 (file)
index 0000000..d20c760
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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.platform.db.migration.version.v73;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.def.IntegerColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+@SupportsBlueGreen
+public class AddIndexOnOrganizationMembers extends DdlChange {
+  public AddIndexOnOrganizationMembers(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(
+      new CreateIndexBuilder(getDialect())
+        .setTable("organization_members")
+        .setName("ix_org_members_on_user_id")
+        .addColumn(IntegerColumnDef.newIntegerColumnDefBuilder()
+          .setColumnName("user_id")
+          .setIsNullable(false)
+          .build())
+        .build());
+  }
+}
index dc159604ae3fc00cd10e88acd4a601da69003100..6611ef0b301abbef1e588fb0470b26d3bb1b0d06 100644 (file)
@@ -39,6 +39,7 @@ public class DbVersion73 implements DbVersion {
       .add(2209, "Fix missing quality profiles on organizations", FixMissingQualityProfilesOnOrganizations.class)
       .add(2210, "Add 'securityhotspotadmin' permission to templates characteristics already having 'issueadmin'", PopulateHotspotAdminPermissionOnTemplatesCharacteristics.class)
       .add(2211, "Set SUBSCRIPTION not nullable in ORGANIZATIONS", SetSubscriptionOnOrganizationsNotNullable.class)
+      .add(2212, "Add index on ORGANIZATION_MEMBERS", AddIndexOnOrganizationMembers.class)
     ;
   }
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest.java
new file mode 100644 (file)
index 0000000..e3ba748
--- /dev/null
@@ -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.platform.db.migration.version.v73;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+public class AddIndexOnOrganizationMembersTest {
+  private static final String TABLE_LOADED_TEMPLATES = "organization_members";
+
+  @Rule
+  public CoreDbTester db = CoreDbTester.createForSchema(AddIndexOnOrganizationMembersTest.class, "organization_members.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private AddIndexOnOrganizationMembers underTest = new AddIndexOnOrganizationMembers(db.database());
+
+  @Test
+  public void execute_adds_index_ix_loaded_templates_type() throws SQLException {
+    underTest.execute();
+
+    db.assertIndex(TABLE_LOADED_TEMPLATES, "ix_org_members_on_user_id", "user_id");
+  }
+
+  @Test
+  public void execute_is_not_reentrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Fail to execute");
+
+    underTest.execute();
+  }
+
+}
index 056e97d85cfff518f2fcd6c79c4a27a90df0dff3..fbd6a8fd21e29bc17dc0ecabf6a372a9742b2ef2 100644 (file)
@@ -35,6 +35,6 @@ public class DbVersion73Test {
 
   @Test
   public void verify_migration_count() {
-    verifyMigrationCount(underTest, 12);
+    verifyMigrationCount(underTest, 13);
   }
 }
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v73/AddIndexOnOrganizationMembersTest/organization_members.sql
new file mode 100644 (file)
index 0000000..b14c169
--- /dev/null
@@ -0,0 +1,6 @@
+CREATE TABLE "ORGANIZATION_MEMBERS" (
+  "ORGANIZATION_UUID" VARCHAR(40) NOT NULL,
+  "USER_ID" INTEGER NOT NULL,
+
+  CONSTRAINT "PK_ORGANIZATION_MEMBERS" PRIMARY KEY ("ORGANIZATION_UUID", "USER_ID")
+);
index 91330f3917e7aae70f202fe2e2f458ea556ac485..3660f91d871f567815f6a5a989e5a7b787fe077e 100644 (file)
@@ -47,6 +47,11 @@ public class SafeModeUserSession extends AbstractUserSession {
     return false;
   }
 
+  @Override
+  protected boolean hasMembershipImpl(OrganizationDto organizationDto) {
+    return false;
+  }
+
   @CheckForNull
   @Override
   public String getLogin() {
@@ -90,9 +95,4 @@ public class SafeModeUserSession extends AbstractUserSession {
   public boolean isSystemAdministrator() {
     return false;
   }
-
-  @Override
-  public boolean hasMembershipImpl(OrganizationDto organization) {
-    return false;
-  }
 }
index e44be5866d74a33f13eaf4aaf37bd84c4c4633b6..655bed2a98d5df702b3a2ad64d889cd1fbaefe17 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.server.issue.ws;
 
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.io.Resources;
 import java.util.List;
 import java.util.Map;
@@ -38,11 +41,14 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
+import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.issue.IssueFinder;
+import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Issues.ChangelogWsResponse;
 import org.sonarqube.ws.Issues.ChangelogWsResponse.Changelog;
 
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.base.Strings.emptyToNull;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 import static org.sonar.core.util.Protobuf.setNullable;
@@ -60,11 +66,13 @@ public class ChangelogAction implements IssuesWsAction {
   private final DbClient dbClient;
   private final IssueFinder issueFinder;
   private final AvatarResolver avatarFactory;
+  private final UserSession userSession;
 
-  public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory) {
+  public ChangelogAction(DbClient dbClient, IssueFinder issueFinder, AvatarResolver avatarFactory, UserSession userSession) {
     this.dbClient = dbClient;
     this.issueFinder = issueFinder;
     this.avatarFactory = avatarFactory;
+    this.userSession = userSession;
   }
 
   @Override
@@ -153,11 +161,25 @@ public class ChangelogAction implements IssuesWsAction {
     private final Map<String, ComponentDto> files;
 
     ChangeLogResults(DbSession dbSession, String issueKey) {
-      IssueDto dbIssue = issueFinder.getByKey(dbSession, issueKey);
-      this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, dbIssue.getKey());
-      List<String> userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList());
-      this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid));
-      this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity()));
+      IssueDto issue = issueFinder.getByKey(dbSession, issueKey);
+      if (isMember(dbSession, issue)) {
+        this.changes = dbClient.issueChangeDao().selectChangelogByIssue(dbSession, issue.getKey());
+        List<String> userUuids = changes.stream().filter(change -> change.userUuid() != null).map(FieldDiffs::userUuid).collect(MoreCollectors.toList());
+        this.users = dbClient.userDao().selectByUuids(dbSession, userUuids).stream().collect(MoreCollectors.uniqueIndex(UserDto::getUuid));
+        this.files = dbClient.componentDao().selectByUuids(dbSession, getFileUuids(changes)).stream().collect(MoreCollectors.uniqueIndex(ComponentDto::uuid, Function.identity()));
+      } else {
+        changes = ImmutableList.of();
+        users = ImmutableMap.of();
+        files = ImmutableMap.of();
+      }
+    }
+
+    private boolean isMember(DbSession dbSession, IssueDto issue) {
+      Optional<ComponentDto> project = dbClient.componentDao().selectByUuid(dbSession, issue.getProjectUuid());
+      checkState(project.isPresent(), "Cannot find the project with uuid %s from issue.id %s", issue.getProjectUuid(), issue.getId());
+      java.util.Optional<OrganizationDto> organization = dbClient.organizationDao().selectByUuid(dbSession, project.get().getOrganizationUuid());
+      checkState(organization.isPresent(), "Cannot find the organization with uuid %s from issue.id %s", project.get().getOrganizationUuid(), issue.getId());
+      return userSession.hasMembership(organization.get());
     }
 
     private Set<String> getFileUuids(List<FieldDiffs> changes) {
index 0b03a534e5244a8f1eca9150f8ffbd1df0220ebf..818a26b7706b440dc463570df55a9e1928e15cdc 100644 (file)
@@ -30,12 +30,14 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 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.elasticsearch.action.search.SearchResponse;
 import org.elasticsearch.search.SearchHit;
+import org.sonar.api.config.Configuration;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
@@ -52,14 +54,15 @@ import org.sonar.api.utils.log.Loggers;
 import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
 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.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.issue.index.IssueQuery;
+import org.sonar.server.issue.index.IssueQueryFactory;
 import org.sonar.server.user.UserSession;
 import org.sonarqube.ws.Issues.SearchWsResponse;
 
@@ -74,12 +77,13 @@ import static java.util.Optional.ofNullable;
 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.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
 import static org.sonar.server.es.SearchOptions.MAX_LIMIT;
 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.IssueQuery.SORT_BY_ASSIGNEE;
 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;
@@ -149,10 +153,11 @@ public class SearchAction implements IssuesWsAction {
   private final SearchResponseFormat searchResponseFormat;
   private final System2 system2;
   private final DbClient dbClient;
+  private final boolean isOnSonarCloud;
 
-  public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory,
-    SearchResponseLoader searchResponseLoader, SearchResponseFormat searchResponseFormat, System2 system2,
-    DbClient dbClient) {
+  public SearchAction(UserSession userSession, IssueIndex issueIndex, IssueQueryFactory issueQueryFactory, SearchResponseLoader searchResponseLoader,
+    SearchResponseFormat searchResponseFormat, Configuration config, System2 system2, DbClient dbClient) {
+    this.isOnSonarCloud = config.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false);
     this.userSession = userSession;
     this.issueIndex = issueIndex;
     this.issueQueryFactory = issueQueryFactory;
@@ -376,10 +381,9 @@ public class SearchAction implements IssuesWsAction {
   }
 
   private List<String> getLogins(Request request) {
-
     List<String> assigneeLogins = request.paramAsStrings(PARAM_ASSIGNEES);
-
     List<String> onlyLogins = new ArrayList<>();
+
     for (String login : ofNullable(assigneeLogins).orElse(emptyList())) {
       if (LOGIN_MYSELF.equals(login)) {
         if (userSession.getLogin() == null) {
@@ -433,13 +437,18 @@ public class SearchAction implements IssuesWsAction {
         .filter(FACETS_REQUIRING_PROJECT_OR_ORGANIZATION::contains)
         .collect(toSet());
       checkArgument(facetsRequiringProjectOrOrganizationParameter.isEmpty() ||
-        (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization",
+          (!query.projectUuids().isEmpty()) || query.organizationUuid() != null, "Facet(s) '%s' require to also filter by project or organization",
         COMA_JOINER.join(facetsRequiringProjectOrOrganizationParameter));
     }
     SearchResponseData preloadedData = new SearchResponseData(emptyList());
     preloadedData.setRules(ImmutableList.copyOf(query.rules()));
     SearchResponseData data = searchResponseLoader.load(preloadedData, collector, facets);
 
+    if (userSession.isLoggedIn()) {
+      try (DbSession dbSession = dbClient.openSession(false)) {
+        data.setUserOrganizationUuids(dbClient.organizationMemberDao().selectOrganizationUuidsByUser(dbSession, userSession.getUserId()));
+      }
+    }
     // format response
 
     // Filter and reorder facets according to the requested ordered names.
@@ -536,10 +545,36 @@ public class SearchAction implements IssuesWsAction {
     }
   }
 
-  private static SearchOptions createSearchOptionsFromRequest(SearchRequest request) {
+  private SearchOptions createSearchOptionsFromRequest(SearchRequest request) {
     SearchOptions options = new SearchOptions();
     options.setPage(request.getPage(), request.getPageSize());
-    options.addFacets(request.getFacets());
+
+    List<String> facets = request.getFacets();
+
+    if (facets == null || facets.isEmpty()) {
+      return options;
+    }
+
+    List<String> requestedFacets = new ArrayList<>(facets.size());
+    requestedFacets.addAll(facets);
+
+    if (isOnSonarCloud) {
+      Optional<OrganizationDto> organizationDto = Optional.empty();
+      String organizationKey = request.getOrganization();
+      if (organizationKey != null) {
+        try (DbSession dbSession = dbClient.openSession(false)) {
+          organizationDto = dbClient.organizationDao().selectByKey(dbSession, organizationKey);
+        }
+      }
+
+      if (!organizationDto.isPresent() || !userSession.hasMembership(organizationDto.get())) {
+        // In order to display the authors facet, the organization parameter must be set and the user
+        // must be member of this organization
+        requestedFacets.remove(PARAM_AUTHORS);
+      }
+    }
+
+    options.addFacets(requestedFacets);
 
     return options;
   }
index e39cbff71efbe93a1c323086fa1cd64fd5671aba..4c8a59d00632320295207995866a3127394d4cb3 100644 (file)
@@ -57,6 +57,7 @@ public class SearchResponseData {
   private final ListMultimap<String, String> actionsByIssueKey = ArrayListMultimap.create();
   private final ListMultimap<String, Transition> transitionsByIssueKey = ArrayListMultimap.create();
   private final Set<String> updatableComments = new HashSet<>();
+  private final Set<String> userOrganizationUuids = new HashSet<>();
 
   public SearchResponseData(IssueDto issue) {
     checkNotNull(issue);
@@ -173,6 +174,14 @@ public class SearchResponseData {
     this.organizationKeysByUuid.put(organizationDto.getUuid(), organizationDto.getKey());
   }
 
+  public void setUserOrganizationUuids(Set<String> organizationUuids) {
+    this.userOrganizationUuids.addAll(organizationUuids);
+  }
+
+  public Set<String> getUserOrganizationUuids() {
+    return this.userOrganizationUuids;
+  }
+
   @CheckForNull
   public UserDto getUserByUuid(@Nullable String userUuid) {
     UserDto userDto = usersByUuid.get(userUuid);
index 2bf22c04931bc92a6efec4251f17c8e1392eb619..5e1e656f669d6ac5a7f15a2ee1802265c7c6b5dc 100644 (file)
@@ -82,8 +82,7 @@ public class SearchResponseFormat {
     this.avatarFactory = avatarFactory;
   }
 
-  public SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data,
-    Paging paging, @Nullable Facets facets) {
+  public SearchWsResponse formatSearch(Set<SearchAdditionalField> fields, SearchResponseData data, Paging paging, @Nullable Facets facets) {
     SearchWsResponse.Builder response = SearchWsResponse.newBuilder();
 
     formatPaging(paging, response);
@@ -195,7 +194,11 @@ public class SearchResponseFormat {
     setNullable(dto.getLine(), issueBuilder::setLine);
     setNullable(emptyToNull(dto.getChecksum()), issueBuilder::setHash);
     completeIssueLocations(dto, issueBuilder, data);
-    issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin()));
+
+    // Filter author only if user is member of the organization
+    if (data.getUserOrganizationUuids().contains(component.getOrganizationUuid())) {
+      issueBuilder.setAuthor(nullToEmpty(dto.getAuthorLogin()));
+    }
     setNullable(dto.getIssueCreationDate(), issueBuilder::setCreationDate, DateUtils::formatDateTime);
     setNullable(dto.getIssueUpdateDate(), issueBuilder::setUpdateDate, DateUtils::formatDateTime);
     setNullable(dto.getIssueCloseDate(), issueBuilder::setCloseDate, DateUtils::formatDateTime);
index 263f4c7539e0c9a53f48fbaf4c650b84afd58691..bd62f627c4c40c0d1de6908ce8190796c112a78b 100644 (file)
@@ -33,6 +33,7 @@ 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.organization.OrganizationDto;
 import org.sonar.db.protobuf.DbFileSources;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.source.HtmlSourceDecorator;
@@ -138,19 +139,24 @@ public class LinesAction implements SourcesWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       ComponentDto file = loadComponent(dbSession, request);
       userSession.checkComponentPermission(UserRole.CODEVIEWER, file);
-
       int from = request.mandatoryParamAsInt(PARAM_FROM);
       int to = MoreObjects.firstNonNull(request.paramAsInt(PARAM_TO), Integer.MAX_VALUE);
 
       Iterable<DbFileSources.Line> lines = checkFoundWithOptional(sourceService.getLines(dbSession, file.uuid(), from, to), "No source found for file '%s'", file.getDbKey());
       try (JsonWriter json = response.newJsonWriter()) {
         json.beginObject();
-        writeSource(lines, json);
+        writeSource(lines, json, isMemberOfOrganization(dbSession, file));
         json.endObject();
       }
     }
   }
 
+  private boolean isMemberOfOrganization(DbSession dbSession, ComponentDto file) {
+    OrganizationDto organizationDto = dbClient.organizationDao().selectByUuid(dbSession, file.getOrganizationUuid())
+      .orElseThrow(() -> new IllegalStateException(String.format("Organization with uuid '%s' not found", file.getOrganizationUuid())));
+    return !userSession.hasMembership(organizationDto);
+  }
+
   private ComponentDto loadComponent(DbSession dbSession, Request wsRequest) {
     String componentKey = wsRequest.param(PARAM_KEY);
     String componentId = wsRequest.param(PARAM_UUID);
@@ -166,14 +172,16 @@ public class LinesAction implements SourcesWsAction {
     return componentFinder.getByKeyAndOptionalBranchOrPullRequest(dbSession, componentKey, branch, pullRequest);
   }
 
-  private void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json) {
+  private void writeSource(Iterable<DbFileSources.Line> lines, JsonWriter json, boolean filterScmAuthors) {
     json.name("sources").beginArray();
     for (DbFileSources.Line line : lines) {
       json.beginObject()
         .prop("line", line.getLine())
         .prop("code", htmlSourceDecorator.getDecoratedSourceAsHtml(line.getSource(), line.getHighlighting(), line.getSymbols()))
-        .prop("scmAuthor", line.getScmAuthor())
         .prop("scmRevision", line.getScmRevision());
+      if (!filterScmAuthors) {
+        json.prop("scmAuthor", line.getScmAuthor());
+      }
       if (line.hasScmDate()) {
         json.prop("scmDate", DateUtils.formatDateTime(new Date(line.getScmDate())));
       }
index b73253e059b458192c63f13c5d83db057de7a49e..1bc20879f51997dfe28db80cfe1ea17a0e9a0d13 100644 (file)
@@ -75,11 +75,11 @@ public abstract class AbstractUserSession implements UserSession {
   protected abstract boolean hasProjectUuidPermission(String permission, String projectUuid);
 
   @Override
-  public final boolean hasMembership(OrganizationDto organization) {
-    return isRoot() || hasMembershipImpl(organization);
+  public final boolean hasMembership(OrganizationDto organizationDto) {
+    return isRoot() || hasMembershipImpl(organizationDto);
   }
 
-  protected abstract boolean hasMembershipImpl(OrganizationDto organization);
+  protected abstract boolean hasMembershipImpl(OrganizationDto organizationDto);
 
   @Override
   public final List<ComponentDto> keepAuthorizedComponents(String permission, Collection<ComponentDto> components) {
index 4f1ccc5b97a4be42e11c33eab115fb66a284bead..434d3731e85d92f2e090b65ab9798e0ad3acb831 100644 (file)
@@ -124,7 +124,7 @@ public final class DoPrivileged {
       }
 
       @Override
-      public boolean hasMembershipImpl(OrganizationDto organization) {
+      public boolean hasMembershipImpl(OrganizationDto organizationDto) {
         return true;
       }
     }
index 504b2929032e44dc1818befc707f982323f22814..e366d1713da747563852428d92e13a927b01df6a 100644 (file)
@@ -228,18 +228,18 @@ public class ServerUserSession extends AbstractUserSession {
   }
 
   @Override
-  public boolean hasMembershipImpl(OrganizationDto organization) {
-    return isMember(organization);
+  public boolean hasMembershipImpl(OrganizationDto organizationDto) {
+    return isMember(organizationDto.getUuid());
   }
 
-  private boolean isMember(OrganizationDto organization) {
+  private boolean isMember(String organizationUuid) {
     if (!isLoggedIn()) {
       return false;
     }
     if (isRoot()) {
       return true;
     }
-    String organizationUuid = organization.getUuid();
+
     if (organizationMembership.contains(organizationUuid)) {
       return true;
     }
index 79287e99fadae7e2d39d28bff4d73c88a491b07e..8b7fb1b6ee26896d504ae70be2c4e02ce7cd3e3a 100644 (file)
@@ -166,8 +166,8 @@ public class ThreadLocalUserSession implements UserSession {
   }
 
   @Override
-  public boolean hasMembership(OrganizationDto organization) {
-    return get().hasMembership(organization);
+  public boolean hasMembership(OrganizationDto organizationDto) {
+    return get().hasMembership(organizationDto);
   }
 
   @Override
index f4463790ac3b4c25f9bcc423e4f45916aeb1628b..f8077aaf54be1d1d104c132ab095a441cc907b1c 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.core.issue.FieldDiffs;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
+import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.rule.RuleDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserTesting;
@@ -67,7 +68,7 @@ public class ChangelogActionTest {
 
   private ComponentDto project;
   private ComponentDto file;
-  private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl()));
+  private WsActionTester tester = new WsActionTester(new ChangelogAction(db.getDbClient(), new IssueFinder(db.getDbClient(), userSession), new AvatarResolverImpl(), userSession));
 
   @Before
   public void setUp() throws Exception {
@@ -79,7 +80,9 @@ public class ChangelogActionTest {
   public void return_changelog() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -92,14 +95,30 @@ public class ChangelogActionTest {
     assertThat(result.getChangelogList().get(0).getDiffsList()).extracting(Diff::getKey, Diff::getOldValue, Diff::getNewValue).containsOnly(tuple("severity", "MAJOR", "BLOCKER"));
   }
 
+  @Test
+  public void return_empty_changelog_when_not_member() {
+    UserDto user = insertUser();
+    IssueDto issueDto = db.issues().insertIssue(newIssue());
+    userSession.logIn("john")
+      .addProjectPermission(USER, project, file);
+    db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()));
+
+    ChangelogWsResponse result = call(issueDto.getKey());
+
+    assertThat(result.getChangelogList()).hasSize(0);
+  }
+
   @Test
   public void changelog_of_file_move_contains_file_names() {
     RuleDto rule = db.rules().insertRule(newRuleDto());
-    ComponentDto project = db.components().insertPrivateProject(db.organizations().insert());
+    OrganizationDto org = db.organizations().insert();
+    ComponentDto project = db.components().insertPrivateProject(org);
     ComponentDto file1 = db.components().insertComponent(newFileDto(project));
     ComponentDto file2 = db.components().insertComponent(newFileDto(project));
     IssueDto issueDto = db.issues().insertIssue(newDto(rule, file2, project));
-    userSession.logIn("john").addProjectPermission(USER, project, file1, file2);
+    userSession.logIn("john")
+      .addMembership(org)
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setDiff("file", file1.uuid(), file2.uuid()).setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -114,7 +133,9 @@ public class ChangelogActionTest {
   @Test
   public void changelog_of_file_move_is_empty_when_files_does_not_exists() {
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setDiff("file", "UNKNOWN_1", "UNKNOWN_2").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -128,7 +149,9 @@ public class ChangelogActionTest {
   public void return_changelog_on_user_without_email() {
     UserDto user = db.users().insertUser(UserTesting.newUserDto("john", "John", null));
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -142,7 +165,9 @@ public class ChangelogActionTest {
   @Test
   public void return_changelog_not_having_user() {
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(null).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -157,7 +182,9 @@ public class ChangelogActionTest {
   @Test
   public void return_changelog_on_none_existing_user() {
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid("UNKNOWN").setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -173,7 +200,9 @@ public class ChangelogActionTest {
   public void return_multiple_diffs() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid())
       .setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())
       .setDiff("status", "RESOLVED", "CLOSED").setCreationDate(new Date()));
@@ -189,7 +218,9 @@ public class ChangelogActionTest {
   public void return_changelog_when_no_old_value() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", null, "BLOCKER").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -202,7 +233,9 @@ public class ChangelogActionTest {
   public void return_changelog_when_no_new_value() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", null).setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -215,7 +248,9 @@ public class ChangelogActionTest {
   public void return_many_changelog() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto,
       new FieldDiffs().setUserUuid(user.getUuid()).setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date()),
       new FieldDiffs().setDiff("status", "RESOLVED", "CLOSED").setCreationDate(new Date()));
@@ -229,7 +264,9 @@ public class ChangelogActionTest {
   public void replace_technical_debt_key_by_effort() {
     UserDto user = insertUser();
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs().setUserUuid(user.getUuid()).setDiff("technicalDebt", "10", "20").setCreationDate(new Date()));
 
     ChangelogWsResponse result = call(issueDto.getKey());
@@ -261,7 +298,9 @@ public class ChangelogActionTest {
   public void test_example() {
     UserDto user = db.users().insertUser(newUserDto("john.smith", "John Smith", "john@smith.com"));
     IssueDto issueDto = db.issues().insertIssue(newIssue());
-    userSession.logIn("john").addProjectPermission(USER, project, file);
+    userSession.logIn("john")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(USER, project, file);
     db.issues().insertFieldDiffs(issueDto, new FieldDiffs()
       .setUserUuid(user.getUuid())
       .setDiff("severity", "MAJOR", "BLOCKER").setCreationDate(new Date())
index 3f88cfabadd7b9d6abe96c6623d8be10b17880b4..4f0576729bdcf6b5b703d4364dcb6990165886f9 100644 (file)
@@ -25,6 +25,7 @@ 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.resources.Qualifiers;
 import org.sonar.api.rule.RuleKey;
@@ -37,13 +38,14 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueDto;
 import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.rule.RuleDefinitionDto;
+import org.sonar.db.user.UserDto;
 import org.sonar.server.es.EsTester;
 import org.sonar.server.issue.IssueFieldsSetter;
-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.index.IssueQueryFactory;
 import org.sonar.server.issue.workflow.FunctionExecutor;
 import org.sonar.server.issue.workflow.IssueWorkflow;
 import org.sonar.server.permission.index.PermissionIndexerTester;
@@ -105,8 +107,8 @@ public class SearchActionComponentsTest {
   private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
   private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
 
-  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, System2.INSTANCE,
-    dbClient));
+  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
+    new MapSettings().asConfig(), System2.INSTANCE, dbClient));
 
   @Test
   public void search_all_issues_when_no_parameter() {
@@ -644,7 +646,7 @@ public class SearchActionComponentsTest {
   }
 
   @Test
-  public void search_by_author() throws Exception {
+  public void search_by_author() {
     ComponentDto project = db.components().insertPublicProject(p -> p.setDbKey("PK1"));
     ComponentDto file = db.components().insertComponent(newFileDto(project, null, "F1").setDbKey("FK1"));
     RuleDefinitionDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.of("xoo", "x1")));
@@ -653,6 +655,10 @@ public class SearchActionComponentsTest {
     allowAnyoneOnProjects(project);
     indexIssues();
 
+    UserDto user = db.users().insertUser();
+    db.organizations().addMember(db.getDefaultOrganization(), user);
+    userSession.logIn(user).addMembership(db.getDefaultOrganization());
+
     ws.newRequest()
       .setParam(IssuesWsParameters.PARAM_AUTHORS, "leia")
       .setParam(WebService.Param.FACETS, "authors")
index dc5b66f172176f1fcd8c225a533e7977b41baaf8..e4a45d4eb9486d3482c2fb176f98ce5570dfa6d0 100644 (file)
@@ -27,6 +27,7 @@ import org.junit.Before;
 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.rule.RuleStatus;
 import org.sonar.api.rules.RuleType;
@@ -98,28 +99,28 @@ import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_RULES;
 
 public class SearchActionTest {
 
-  @Rule
-  public UserSessionRule userSessionRule = standalone();
-  @Rule
-  public DbTester db = DbTester.create();
-  @Rule
-  public EsTester es = EsTester.create();
-  @Rule
-  public ExpectedException expectedException = none();
-
-  private DbClient dbClient = db.getDbClient();
-  private DbSession session = db.getSession();
-  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();
-  private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
-  private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSessionRule, dbClient, new TransitionService(userSessionRule, issueWorkflow));
-  private Languages languages = new Languages();
-  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
-  private WsActionTester ws = new WsActionTester(new SearchAction(userSessionRule, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat, System2.INSTANCE,
-    dbClient));
-  private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
+    @Rule
+    public UserSessionRule userSession = standalone();
+    @Rule
+    public DbTester db = DbTester.create();
+    @Rule
+    public EsTester es = EsTester.create();
+    @Rule
+    public ExpectedException expectedException = none();
+
+    private DbClient dbClient = db.getDbClient();
+    private DbSession session = db.getSession();
+    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 IssueQueryFactory issueQueryFactory = new IssueQueryFactory(dbClient, Clock.systemUTC(), userSession);
+    private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter();
+    private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
+    private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
+    private Languages languages = new Languages();
+    private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+    private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
+      new MapSettings().asConfig(), System2.INSTANCE, dbClient));
+    private StartupIndexer permissionIndexer = new PermissionIndexer(dbClient, es.client(), issueIndexer);
 
   private OrganizationDto defaultOrganization;
   private OrganizationDto otherOrganization1;
@@ -248,6 +249,7 @@ public class SearchActionTest {
   @Test
   public void response_contains_all_fields_except_additional_fields() {
     UserDto simon = db.users().insertUser(u -> u.setLogin("simon").setName("Simon").setEmail("simon@email.com"));
+    db.organizations().addMember(otherOrganization2, simon);
 
     ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setDbKey("PROJECT_KEY"));
     indexPermissions();
@@ -269,9 +271,34 @@ public class SearchActionTest {
     db.issues().insertIssue(issue);
     indexIssues();
 
+    userSession.logIn(simon);
     ws.newRequest().execute().assertJson(this.getClass(), "response_contains_all_fields_except_additional_fields.json");
   }
 
+  @Test
+  public void hide_author_if_not_member_of_organization() {
+    ComponentDto project = insertComponent(ComponentTesting.newPublicProjectDto(otherOrganization2, "PROJECT_ID").setDbKey("PROJECT_KEY"));
+    indexPermissions();
+    ComponentDto file = insertComponent(newFileDto(project, null, "FILE_ID").setDbKey("FILE_KEY"));
+    IssueDto issue = newDto(newExternalRule(), file, project)
+      .setKee("82fd47d4-b650-4037-80bc-7b112bd4eac2")
+      .setEffort(10L)
+      .setLine(42)
+      .setChecksum("a227e508d6646b55a086ee11d63b21e9")
+      .setMessage("the message")
+      .setStatus(STATUS_RESOLVED)
+      .setResolution(RESOLUTION_FIXED)
+      .setSeverity("MAJOR")
+      .setAuthorLogin("John")
+      .setTags(asList("bug", "owasp"))
+      .setIssueCreationDate(DateUtils.parseDateTime("2014-09-04T00:00:00+0100"))
+      .setIssueUpdateDate(DateUtils.parseDateTime("2017-12-04T00:00:00+0100"));
+    db.issues().insertIssue(issue);
+    indexIssues();
+
+    ws.newRequest().execute().assertJson(this.getClass(), "author_is_hidden.json");
+  }
+
   @Test
   public void issue_with_cross_file_locations() {
     UserDto simon = db.users().insertUser();
@@ -369,7 +396,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
     ws.newRequest()
       .setParam("additionalFields", "comments,users")
       .execute()
@@ -405,7 +432,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
     TestResponse result = ws.newRequest().setParam(PARAM_HIDE_COMMENTS, "true").execute();
     result.assertJson(this.getClass(), "issue_with_comment_hidden.json");
     assertThat(result.getInput()).doesNotContain(fabrice.getLogin());
@@ -426,7 +453,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn("john");
+    userSession.logIn("john");
     ws.newRequest()
       .setParam("additionalFields", "_all").execute()
       .assertJson(this.getClass(), "load_additional_fields.json");
@@ -450,7 +477,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn("john")
+    userSession.logIn("john")
       .addProjectPermission(ISSUE_ADMIN, project); // granted by Anyone
     ws.newRequest()
       .setParam("additionalFields", "_all").execute()
@@ -468,7 +495,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn("john")
+    userSession.logIn("john")
       .addProjectPermission(ISSUE_ADMIN, project); // granted by Anyone
     indexPermissions();
 
@@ -552,7 +579,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn("john");
+    userSession.logIn("john");
     ws.newRequest()
       .setParam("resolved", "false")
       .setParam(PARAM_COMPONENT_KEYS, project.getKey())
@@ -579,7 +606,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
     ws.newRequest()
       .setParam("resolved", "false")
       .setParam(PARAM_COMPONENT_KEYS, project.getKey())
@@ -607,7 +634,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
     ws.newRequest()
       .setParam(PARAM_COMPONENT_KEYS, project.getKey())
       .setParam("resolved", "false")
@@ -623,7 +650,7 @@ public class SearchActionTest {
     // login looks like an invalid regexp
     UserDto user = db.users().insertUser(u -> u.setLogin("foo[").setName("foo").setEmail("foo@email.com"));
 
-    userSessionRule.logIn(user);
+    userSession.logIn(user);
 
     // should not fail
     ws.newRequest()
@@ -670,7 +697,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
 
     ws.newRequest()
       .setParam("resolved", "false")
@@ -718,7 +745,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
 
     Issues.SearchWsResponse response = ws.newRequest()
       .setParam("resolved", "false")
@@ -732,7 +759,7 @@ public class SearchActionTest {
   @Test
   public void filter_by_assigned_to_me_unauthenticated() {
     UserDto poy = db.users().insertUser(u -> u.setLogin("poy").setName("poypoy").setEmail("poypoy@email.com"));
-    userSessionRule.logIn(poy);
+    userSession.logIn(poy);
 
     // TODO : check test title w julien
 
@@ -803,7 +830,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn(john);
+    userSession.logIn(john);
     ws.newRequest()
       .setParam("resolved", "false")
       .setParam("assignees", "alice")
@@ -925,7 +952,7 @@ public class SearchActionTest {
     session.commit();
     indexIssues();
 
-    userSessionRule.logIn("john");
+    userSession.logIn("john");
     ws.newRequest()
       .setParam("resolved", "false")
       .setParam(WebService.Param.FACETS, "severities")
@@ -979,7 +1006,7 @@ public class SearchActionTest {
         .setResourceId(project.getId())
         .setRole(permission));
     session.commit();
-    userSessionRule.logIn().addProjectPermission(permission, project);
+    userSession.logIn().addProjectPermission(permission, project);
   }
 
   private ComponentDto insertComponent(ComponentDto component) {
diff --git a/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java b/server/sonar-server/src/test/java/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud.java
new file mode 100644 (file)
index 0000000..5c38990
--- /dev/null
@@ -0,0 +1,167 @@
+package org.sonar.server.issue.ws;/*
+ * 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.
+ */
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+import java.time.Clock;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.api.resources.Languages;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.utils.Durations;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+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.UserDto;
+import org.sonar.server.es.EsTester;
+import org.sonar.server.issue.IssueFieldsSetter;
+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.index.IssueQueryFactory;
+import org.sonar.server.issue.workflow.FunctionExecutor;
+import org.sonar.server.issue.workflow.IssueWorkflow;
+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.sonar.server.ws.WsResponseCommonFormat;
+import org.sonar.test.JsonAssert;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.server.ws.WebService.Param.FACETS;
+import static org.sonar.db.component.ComponentTesting.newFileDto;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ORGANIZATION;
+import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_PROJECT_UUIDS;
+
+public class SearchActionTestOnSonarCloud {
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create();
+  @Rule
+  public EsTester es = EsTester.create();
+
+  private MapSettings mapSettings = new MapSettings()
+    .setProperty("sonar.sonarcloud.enabled", true);
+
+  private DbClient dbClient = db.getDbClient();
+  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);
+  private IssueFieldsSetter issueFieldsSetter = new IssueFieldsSetter();
+  private IssueWorkflow issueWorkflow = new IssueWorkflow(new FunctionExecutor(issueFieldsSetter), issueFieldsSetter);
+  private SearchResponseLoader searchResponseLoader = new SearchResponseLoader(userSession, dbClient, new TransitionService(userSession, issueWorkflow));
+  private Languages languages = new Languages();
+  private SearchResponseFormat searchResponseFormat = new SearchResponseFormat(new Durations(), new WsResponseCommonFormat(languages), languages, new AvatarResolverImpl());
+  private PermissionIndexerTester permissionIndexer = new PermissionIndexerTester(es, issueIndexer);
+
+  private WsActionTester ws = new WsActionTester(new SearchAction(userSession, issueIndex, issueQueryFactory, searchResponseLoader, searchResponseFormat,
+    mapSettings.asConfig(), System2.INSTANCE, dbClient));
+
+  private OrganizationDto organization;
+  private UserDto user;
+  private ComponentDto project;
+
+  @Before
+  public void setup() {
+    organization = db.organizations().insert(o -> o.setKey("org-1"));
+    user = db.users().insertUser();
+
+    project = db.components().insertPublicProject(organization, p -> p.setDbKey("PK1"));
+    ComponentDto file = db.components().insertComponent(newFileDto(project, null, "F1").setDbKey("FK1"));
+    RuleDefinitionDto rule = db.rules().insert(r -> r.setRuleKey(RuleKey.of("xoo", "x1")));
+    db.issues().insert(rule, project, file, i -> i.setAuthorLogin("leia").setKee("2bd4eac2-b650-4037-80bc-7b112bd4eac2"));
+    db.issues().insert(rule, project, file, i -> i.setAuthorLogin("luke@skywalker.name").setKee("82fd47d4-b650-4037-80bc-7b1182fd47d4"));
+    db.commit();
+    allowAnyoneOnProjects(project);
+    indexIssues();
+  }
+
+  @Test
+  public void authors_facet_is_hidden_if_organization_is_not_set() {
+    db.organizations().addMember(organization, user);
+    userSession
+      .logIn(user)
+      .addMembership(organization);
+
+    String input = ws.newRequest()
+      .setParam(PARAM_PROJECT_UUIDS, project.uuid())
+      .setParam(FACETS, "authors")
+      .execute()
+      .getInput();
+
+    JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_authors_facet.json"));
+
+    JsonElement gson = new JsonParser().parse(input);
+    assertThat(gson.getAsJsonObject().get("facets")).isNull();
+  }
+
+  @Test
+  public void authors_facet_is_hidden_if_user_is_not_a_member_of_the_organization() {
+    userSession
+      .logIn(user);
+
+    String input = ws.newRequest()
+      .setParam(PARAM_PROJECT_UUIDS, project.uuid())
+      .setParam(FACETS, "authors")
+      .execute()
+      .getInput();
+
+    JsonAssert.assertJson(input).isSimilarTo(this.getClass().getResource(this.getClass().getSimpleName() + "/no_author_and_no_authors_facet.json"));
+
+    JsonElement gson = new JsonParser().parse(input);
+    assertThat(gson.getAsJsonObject().get("facets")).isNull();
+
+  }
+
+  @Test
+  public void authors_facet_is_shown_if_organization_is_set_and_user_is_member_of_the_organization() {
+    db.organizations().addMember(organization, user);
+
+    userSession
+      .logIn(user)
+      .addMembership(organization);
+
+    ws.newRequest()
+      .setParam(PARAM_PROJECT_UUIDS, project.uuid())
+      .setParam(FACETS, "authors")
+      .setParam(PARAM_ORGANIZATION, organization.getKey())
+      .execute()
+      .assertJson(this.getClass(), "with_authors_facet.json");
+  }
+
+  private void indexIssues() {
+    issueIndexer.indexOnStartup(null);
+  }
+
+  private void allowAnyoneOnProjects(ComponentDto... projects) {
+    userSession.registerComponents(projects);
+    Arrays.stream(projects).forEach(p -> permissionIndexer.allowOnlyAnyone(p));
+  }
+}
index 25cfea083deb8c5937d5dc1b7b7e9a9de1fa4086..70f0811c18587f09b8641adffe6b5e7df9627df7 100644 (file)
  */
 package org.sonar.server.source.ws;
 
-import java.io.IOException;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
-import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 import org.sonar.api.utils.System2;
 import org.sonar.api.web.UserRole;
@@ -32,8 +30,10 @@ import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDao;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.organization.OrganizationDto;
 import org.sonar.db.protobuf.DbFileSources;
 import org.sonar.db.source.FileSourceDto;
+import org.sonar.db.user.UserDto;
 import org.sonar.server.component.TestComponentFinder;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
@@ -42,6 +42,7 @@ import org.sonar.server.source.SourceService;
 import org.sonar.server.source.index.FileSourceTesting;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsTester;
+import org.sonar.server.ws.WsTester.TestRequest;
 
 import static java.lang.String.format;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -52,74 +53,60 @@ import static org.sonar.db.component.ComponentTesting.newFileDto;
 
 public class LinesActionTest {
 
-  private static final String PROJECT_UUID = "abcd";
-  private static final String FILE_UUID = "efgh";
-  private static final String FILE_KEY = "Foo.java";
-
   @Rule
   public ExpectedException expectedException = ExpectedException.none();
-
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
-
   @Rule
   public UserSessionRule userSession = UserSessionRule.standalone();
 
-  private SourceService sourceService;
-  private HtmlSourceDecorator htmlSourceDecorator;
-  private ComponentDao componentDao;
-
-  private ComponentDto project;
-  private ComponentDto file;
-
+  private ComponentDao componentDao = new ComponentDao();
+  private ComponentDto privateProject;
+  private OrganizationDto organization;
   private WsTester wsTester;
 
   @Before
   public void setUp() {
-    htmlSourceDecorator = mock(HtmlSourceDecorator.class);
-    when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then(new Answer<String>() {
-      @Override
-      public String answer(InvocationOnMock invocationOnMock) {
-        return "<p>" + invocationOnMock.getArguments()[0] + "</p>";
-      }
-    });
-    sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator);
-    componentDao = new ComponentDao();
+    HtmlSourceDecorator htmlSourceDecorator = mock(HtmlSourceDecorator.class);
+    when(htmlSourceDecorator.getDecoratedSourceAsHtml(anyString(), anyString(), anyString())).then((Answer<String>)
+      invocationOnMock -> "<p>" + invocationOnMock.getArguments()[0] + "</p>");
+    SourceService sourceService = new SourceService(db.getDbClient(), htmlSourceDecorator);
     wsTester = new WsTester(new SourcesWs(
       new LinesAction(TestComponentFinder.from(db), db.getDbClient(), sourceService, htmlSourceDecorator, userSession)));
-    project = ComponentTesting.newPrivateProjectDto(db.organizations().insert(), PROJECT_UUID);
-    file = newFileDto(project, null, FILE_UUID).setDbKey(FILE_KEY);
+    organization = db.organizations().insert();
+    privateProject = ComponentTesting.newPrivateProjectDto(organization);
   }
 
   @Test
   public void show_source() throws Exception {
-    insertFileWithData(FileSourceTesting.newFakeData(3).build());
-    setUserWithValidPermission();
+    ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(3).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID);
+    TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", file.uuid());
     request.execute().assertJson(getClass(), "show_source.json");
   }
 
   @Test
   public void fail_to_show_source_if_no_source_found() throws Exception {
-    setUserWithValidPermission();
-    insertFile();
+    ComponentDto file = insertFile(privateProject);
+    setUserWithValidPermission(file);
 
     expectedException.expect(NotFoundException.class);
-    wsTester.newGetRequest("api/sources", "lines").setParam("uuid", FILE_UUID).execute();
+    wsTester.newGetRequest("api/sources", "lines").setParam("uuid", file.uuid()).execute();
   }
 
   @Test
   public void show_paginated_lines() throws Exception {
-    setUserWithValidPermission();
-    insertFileWithData(FileSourceTesting.newFakeData(3).build());
+    ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(3).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester
+    wsTester
       .newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID)
+      .setParam("uuid", file.uuid())
       .setParam("from", "3")
-      .setParam("to", "3");
-    request.execute().assertJson(getClass(), "show_paginated_lines.json");
+      .setParam("to", "3")
+      .execute()
+      .assertJson(getClass(), "show_paginated_lines.json");
   }
 
   @Test
@@ -133,13 +120,16 @@ public class LinesActionTest {
       .setFileUuid(file.uuid())
       .setSourceData(FileSourceTesting.newFakeData(3).build()));
     db.commit();
-    userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file);
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines")
-      .setParam("key", file.getKey())
-      .setParam("branch", file.getBranch());
+    userSession.logIn("login")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(UserRole.CODEVIEWER, project, file);
 
-    request.execute().assertJson(getClass(), "show_source.json");
+    wsTester.newGetRequest("api/sources", "lines")
+      .setParam("key", file.getKey())
+      .setParam("branch", file.getBranch())
+      .execute()
+      .assertJson(getClass(), "show_source.json");
   }
 
   @Test
@@ -153,13 +143,16 @@ public class LinesActionTest {
       .setFileUuid(file.uuid())
       .setSourceData(FileSourceTesting.newFakeData(3).build()));
     db.commit();
-    userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file);
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines")
-      .setParam("key", file.getKey())
-      .setParam("pullRequest", file.getPullRequest());
+    userSession.logIn("login")
+      .addMembership(db.getDefaultOrganization())
+      .addProjectPermission(UserRole.CODEVIEWER, project, file);
 
-    request.execute().assertJson(getClass(), "show_source.json");
+    wsTester.newGetRequest("api/sources", "lines")
+      .setParam("key", file.getKey())
+      .setParam("pullRequest", file.getPullRequest())
+      .execute()
+      .assertJson(getClass(), "show_source.json");
   }
 
   @Test
@@ -167,7 +160,7 @@ public class LinesActionTest {
     expectedException.expect(IllegalArgumentException.class);
     expectedException.expectMessage("Either 'uuid' or 'key' must be provided");
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines");
+    TestRequest request = wsTester.newGetRequest("api/sources", "lines");
     request.execute();
   }
 
@@ -176,7 +169,7 @@ public class LinesActionTest {
     expectedException.expect(NotFoundException.class);
     expectedException.expectMessage("Component key 'Foo.java' not found");
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", FILE_KEY);
+    TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "Foo.java");
     request.execute();
   }
 
@@ -185,50 +178,50 @@ public class LinesActionTest {
     expectedException.expect(NotFoundException.class);
     expectedException.expectMessage("Component id 'ABCD' not found");
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", "ABCD");
+    TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("uuid", "ABCD");
     request.execute();
   }
 
   @Test
   public void fail_when_file_is_removed() throws Exception {
-    ComponentDto file = newFileDto(project).setDbKey("file-key").setEnabled(false);
-    db.components().insertComponents(project, file);
-    setUserWithValidPermission();
+    ComponentDto file = newFileDto(privateProject).setDbKey("file-key").setEnabled(false);
+    db.components().insertComponents(privateProject, file);
+    setUserWithValidPermission(file);
 
     expectedException.expect(NotFoundException.class);
     expectedException.expectMessage("Component key 'file-key' not found");
 
-    WsTester.TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "file-key");
+    TestRequest request = wsTester.newGetRequest("api/sources", "lines").setParam("key", "file-key");
     request.execute();
   }
 
   @Test(expected = ForbiddenException.class)
   public void check_permission() throws Exception {
-    insertFileWithData(FileSourceTesting.newFakeData(1).build());
+    ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(1).build(), privateProject);
 
     userSession.logIn("login");
 
     wsTester.newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID)
+      .setParam("uuid", file.uuid())
       .execute();
   }
 
   @Test
   public void display_deprecated_fields() throws Exception {
-    insertFileWithData(FileSourceTesting.newFakeData(1).build());
-    setUserWithValidPermission();
+    ComponentDto file = insertFileWithData(FileSourceTesting.newFakeData(1).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester
+    wsTester
       .newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID);
-
-    request.execute().assertJson(getClass(), "display_deprecated_fields.json");
+      .setParam("uuid", file.uuid())
+      .execute()
+      .assertJson(getClass(), "display_deprecated_fields.json");
   }
 
   @Test
   public void use_deprecated_overall_coverage_fields_if_exists() throws Exception {
     DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder();
-    insertFileWithData(dataBuilder.addLines(newLineBuilder()
+    ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder()
       .setDeprecatedOverallLineHits(1)
       .setDeprecatedOverallConditions(2)
       .setDeprecatedOverallCoveredConditions(3)
@@ -237,31 +230,31 @@ public class LinesActionTest {
       .setDeprecatedUtCoveredConditions(3)
       .setDeprecatedItLineHits(1)
       .setDeprecatedItConditions(2)
-      .setDeprecatedItCoveredConditions(3)).build());
-    setUserWithValidPermission();
+      .setDeprecatedItCoveredConditions(3)).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester
+    wsTester
       .newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID);
-
-    request.execute().assertJson(getClass(), "convert_deprecated_data.json");
+      .setParam("uuid", file.uuid())
+      .execute()
+      .assertJson(getClass(), "convert_deprecated_data.json");
   }
 
   @Test
   public void use_deprecated_ut_coverage_fields_if_exists() throws Exception {
     DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder();
-    insertFileWithData(dataBuilder.addLines(newLineBuilder()
+    ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder()
       .setDeprecatedUtLineHits(1)
       .setDeprecatedUtConditions(2)
       .setDeprecatedUtCoveredConditions(3)
       .setDeprecatedItLineHits(1)
       .setDeprecatedItConditions(2)
-      .setDeprecatedItCoveredConditions(3)).build());
-    setUserWithValidPermission();
+      .setDeprecatedItCoveredConditions(3)).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester
+    TestRequest request = wsTester
       .newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID);
+      .setParam("uuid", file.uuid());
 
     request.execute().assertJson(getClass(), "convert_deprecated_data.json");
   }
@@ -269,15 +262,15 @@ public class LinesActionTest {
   @Test
   public void use_deprecated_it_coverage_fields_if_exists() throws Exception {
     DbFileSources.Data.Builder dataBuilder = DbFileSources.Data.newBuilder();
-    insertFileWithData(dataBuilder.addLines(newLineBuilder()
+    ComponentDto file = insertFileWithData(dataBuilder.addLines(newLineBuilder()
       .setDeprecatedItLineHits(1)
       .setDeprecatedItConditions(2)
-      .setDeprecatedItCoveredConditions(3)).build());
-    setUserWithValidPermission();
+      .setDeprecatedItCoveredConditions(3)).build(), privateProject);
+    setUserWithValidPermission(file);
 
-    WsTester.TestRequest request = wsTester
+    TestRequest request = wsTester
       .newGetRequest("api/sources", "lines")
-      .setParam("uuid", FILE_UUID);
+      .setParam("uuid", file.uuid());
 
     request.execute().assertJson(getClass(), "convert_deprecated_data.json");
   }
@@ -342,22 +335,66 @@ public class LinesActionTest {
       .execute();
   }
 
-  private void insertFileWithData(DbFileSources.Data fileData) throws IOException {
-    insertFile();
+  @Test
+  public void hide_scmAuthors_if_not_member_of_organization() throws Exception {
+    OrganizationDto org = db.organizations().insert();
+    ComponentDto publicProject = db.components().insertPublicProject(org);
+    userSession.registerComponents(publicProject);
+
+    DbFileSources.Data data = DbFileSources.Data.newBuilder()
+      .addLines(newLineBuilder().setScmAuthor("isaac@asimov.com"))
+      .build();
+
+    ComponentDto file = insertFileWithData(data, publicProject);
+
+    wsTester.newGetRequest("api/sources", "lines")
+      .setParam("uuid", file.uuid())
+      .execute()
+      .assertJson(getClass(), "hide_scmAuthors.json");
+  }
+
+  @Test
+  public void show_scmAuthors_if_member_of_organization() throws Exception {
+    OrganizationDto org = db.organizations().insert();
+    ComponentDto publicProject = db.components().insertPublicProject(org);
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user)
+      .registerComponents(publicProject)
+      .addMembership(org);
+
+    DbFileSources.Data data = DbFileSources.Data.newBuilder()
+      .addLines(newLineBuilder().setScmAuthor("isaac@asimov.com"))
+      .build();
+
+    ComponentDto file = insertFileWithData(data, publicProject);
+
+    wsTester.newGetRequest("api/sources", "lines")
+      .setParam("uuid", file.uuid())
+      .execute()
+      .assertJson(getClass(), "show_scmAuthors.json");
+  }
+
+  private ComponentDto insertFileWithData(DbFileSources.Data fileData, ComponentDto project) {
+    ComponentDto file = insertFile(project);
     db.getDbClient().fileSourceDao().insert(db.getSession(), new FileSourceDto()
-      .setProjectUuid(PROJECT_UUID)
-      .setFileUuid(FILE_UUID)
+      .setProjectUuid(project.projectUuid())
+      .setFileUuid(file.uuid())
       .setSourceData(fileData));
     db.commit();
+    return file;
   }
 
-  private void setUserWithValidPermission() {
-    userSession.logIn("login").addProjectPermission(UserRole.CODEVIEWER, project, file);
+  private void setUserWithValidPermission(ComponentDto file) {
+    userSession.logIn("login")
+      .addProjectPermission(UserRole.CODEVIEWER, privateProject, file)
+      .addMembership(organization);
   }
 
-  private void insertFile() {
-    componentDao.insert(db.getSession(), project, file);
+  private ComponentDto insertFile(ComponentDto project) {
+    ComponentDto file = newFileDto(project);
+    componentDao.insert(db.getSession(), file);
     db.getSession().commit();
+    return file;
   }
 
   private DbFileSources.Line.Builder newLineBuilder() {
index 942f2336cefac5180b8b8b721984acdda04e0183..785ad3c89634b506c3bacc2858f71dac36f84dac 100644 (file)
@@ -110,8 +110,8 @@ public abstract class AbstractMockUserSession<T extends AbstractMockUserSession>
   }
 
   @Override
-  protected boolean hasMembershipImpl(OrganizationDto organization) {
-    return organizationMembership.contains(organization.getUuid());
+  protected boolean hasMembershipImpl(OrganizationDto organizationDto) {
+    return organizationMembership.contains(organizationDto.getUuid());
   }
 
   public void addOrganizationMembership(OrganizationDto organization) {
index 98af9348a66907365714528d0dab77a0efe1f077..e080df152cc888189cc0f48382925bc37ed597f3 100644 (file)
@@ -65,7 +65,7 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM
   }
 
   @Override
-  public boolean hasMembershipImpl(OrganizationDto organization) {
+  public boolean hasMembershipImpl(OrganizationDto organizationDto) {
     return false;
   }
 }
index d714a8d02625c3d0424d593090a7e398c521523a..7b055ab52a2924e5e0ad866d3aa4c8fac53f4f41 100644 (file)
@@ -118,7 +118,7 @@ public class TestUserSessionFactory implements UserSessionFactory {
     }
 
     @Override
-    public boolean hasMembershipImpl(OrganizationDto organization) {
+    public boolean hasMembershipImpl(OrganizationDto organizationDto) {
       throw notImplemented();
     }
 
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTest/author_is_hidden.json
new file mode 100644 (file)
index 0000000..357e05c
--- /dev/null
@@ -0,0 +1,24 @@
+{
+  "issues": [
+    {
+      "organization": "my-org-2",
+      "key": "82fd47d4-b650-4037-80bc-7b112bd4eac2",
+      "rule": "external_xoo:x1",
+      "severity": "MAJOR",
+      "component": "FILE_KEY",
+      "resolution": "FIXED",
+      "status": "RESOLVED",
+      "message": "the message",
+      "effort": "10min",
+      "line": 42,
+      "hash": "a227e508d6646b55a086ee11d63b21e9",
+      "tags": [
+        "bug",
+        "owasp"
+      ],
+      "creationDate": "2014-09-04T01:00:00+0200",
+      "updateDate": "2017-12-04T00:00:00+0100",
+      "externalRuleEngine" : "xoo"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_author_and_no_authors_facet.json
new file mode 100644 (file)
index 0000000..8f82c80
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "total": 2,
+  "p": 1,
+  "issues": [
+    {
+      "organization": "org-1",
+      "key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1"
+    },
+    {
+      "organization": "org-1",
+      "key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/no_authors_facet.json
new file mode 100644 (file)
index 0000000..c8df191
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "total": 2,
+  "p": 1,
+  "issues": [
+    {
+      "organization": "org-1",
+      "key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1",
+      "author": "luke@skywalker.name"
+    },
+    {
+      "organization": "org-1",
+      "key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1",
+      "author": "leia"
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json b/server/sonar-server/src/test/resources/org/sonar/server/issue/ws/SearchActionTestOnSonarCloud/with_authors_facet.json
new file mode 100644 (file)
index 0000000..cf2a13c
--- /dev/null
@@ -0,0 +1,37 @@
+{
+  "total": 2,
+  "p": 1,
+  "issues": [
+    {
+      "organization": "org-1",
+      "key": "82fd47d4-b650-4037-80bc-7b1182fd47d4",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1",
+      "author": "luke@skywalker.name"
+    },
+    {
+      "organization": "org-1",
+      "key": "2bd4eac2-b650-4037-80bc-7b112bd4eac2",
+      "rule": "xoo:x1",
+      "component": "FK1",
+      "project": "PK1",
+      "author": "leia"
+    }
+  ],
+  "facets": [
+    {
+      "property": "authors",
+      "values": [
+        {
+          "val": "leia",
+          "count": 1
+        },
+        {
+          "val": "luke@skywalker.name",
+          "count": 1
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/hide_scmAuthors.json
new file mode 100644 (file)
index 0000000..d1c3b51
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "sources": [
+    {
+      "line": 1,
+      "code": "\u003cp\u003eSOURCE_1\u003c/p\u003e",
+      "scmRevision": "REVISION_1",
+      "scmDate": "1974-10-03T03:40:00+0100",
+      "duplicated": false
+    }
+  ]
+}
diff --git a/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json b/server/sonar-server/src/test/resources/org/sonar/server/source/ws/LinesActionTest/show_scmAuthors.json
new file mode 100644 (file)
index 0000000..f2f362c
--- /dev/null
@@ -0,0 +1,12 @@
+{
+  "sources": [
+    {
+      "line": 1,
+      "code": "\u003cp\u003eSOURCE_1\u003c/p\u003e",
+      "scmAuthor": "isaac@asimov.com",
+      "scmRevision": "REVISION_1",
+      "scmDate": "1974-10-03T03:40:00+0100",
+      "duplicated": false
+    }
+  ]
+}
index 2faa52930e4a800f2240b248bf434337912bb420..12e2a0f96d183c299c2c2cf517f05eec8f920113 100644 (file)
@@ -26,5 +26,5 @@ interface Props {
 }
 
 export default function ExploreIssues(props: Props) {
-  return <AppContainer myIssues={false} {...props} />;
+  return <AppContainer hideAuthorFacet={true} myIssues={false} {...props} />;
 }
index dd2288d834697ebcf5a84514b7439bdf3fe8e1ab..e4486728c140b6e6494233d81953af2eed24a075 100644 (file)
@@ -52,7 +52,14 @@ import {
   serializeQuery,
   STANDARDS
 } from '../utils';
-import { Component, CurrentUser, Issue, Paging, BranchLike } from '../../../app/types';
+import {
+  Component,
+  CurrentUser,
+  Issue,
+  Paging,
+  BranchLike,
+  Organization
+} from '../../../app/types';
 import handleRequiredAuthentication from '../../../app/utils/handleRequiredAuthentication';
 import Dropdown from '../../../components/controls/Dropdown';
 import ListFooter from '../../../components/controls/ListFooter';
@@ -92,10 +99,12 @@ interface Props {
   component?: Component;
   currentUser: CurrentUser;
   fetchIssues: (query: RawQuery, requestOrganizations?: boolean) => Promise<FetchIssuesPromise>;
+  hideAuthorFacet?: boolean;
   location: { pathname: string; query: RawQuery };
   myIssues?: boolean;
   onBranchesChange: () => void;
   organization?: { key: string };
+  userOrganizations: Organization[];
 }
 
 export interface State {
@@ -407,7 +416,7 @@ export default class App extends React.PureComponent<Props, State> {
     requestFacets = false,
     requestOrganizations = true
   ): Promise<FetchIssuesPromise> => {
-    const { component, organization } = this.props;
+    const { component } = this.props;
     const { myIssues, openFacets, query } = this.state;
 
     const facets = requestFacets
@@ -418,13 +427,17 @@ export default class App extends React.PureComponent<Props, State> {
           .join(',')
       : undefined;
 
+    const organizationKey =
+      (component && component.organization) ||
+      (this.props.organization && this.props.organization.key);
+
     const parameters = {
       ...getBranchLikeQuery(this.props.branchLike),
       componentKeys: component && component.key,
       s: 'FILE_LINE',
       ...serializeQuery(query),
       ps: '100',
-      organization: organization && organization.key,
+      organization: organizationKey,
       facets,
       ...additional
     };
@@ -860,9 +873,21 @@ export default class App extends React.PureComponent<Props, State> {
   }
 
   renderFacets() {
-    const { component, currentUser } = this.props;
+    const { component, currentUser, userOrganizations } = this.props;
     const { query } = this.state;
 
+    const organizationKey =
+      (component && component.organization) ||
+      (this.props.organization && this.props.organization.key);
+
+    const userOrganization =
+      !isSonarCloud() ||
+      userOrganizations.find(o => {
+        return o.key === organizationKey;
+      });
+    const hideAuthorFacet =
+      this.props.hideAuthorFacet || (isSonarCloud() && this.props.myIssues) || !userOrganization;
+
     return (
       <div className="layout-page-filters">
         {currentUser.isLoggedIn &&
@@ -876,6 +901,7 @@ export default class App extends React.PureComponent<Props, State> {
         <Sidebar
           component={component}
           facets={this.state.facets}
+          hideAuthorFacet={hideAuthorFacet}
           loading={this.state.loading}
           loadingFacets={this.state.loadingFacets}
           myIssues={this.state.myIssues}
index 7bc1a48e240a3d9183e00c346d6cb0e35d18005d..683a016303f52cd3d9ee8321b934c2e37f62e204 100644 (file)
@@ -22,9 +22,13 @@ import { Dispatch } from 'redux';
 import { uniq } from 'lodash';
 import { searchIssues } from '../../../api/issues';
 import { getOrganizations } from '../../../api/organizations';
-import { CurrentUser } from '../../../app/types';
+import { CurrentUser, Organization } from '../../../app/types';
 import throwGlobalError from '../../../app/utils/throwGlobalError';
-import { getCurrentUser, areThereCustomOrganizations } from '../../../store/rootReducer';
+import {
+  getCurrentUser,
+  areThereCustomOrganizations,
+  getMyOrganizations
+} from '../../../store/rootReducer';
 import { lazyLoad } from '../../../components/lazyLoad';
 import { parseIssueFromResponse } from '../../../helpers/issues';
 import { RawQuery } from '../../../helpers/query';
@@ -32,10 +36,12 @@ import { receiveOrganizations } from '../../../store/organizations/duck';
 
 interface StateProps {
   currentUser: CurrentUser;
+  userOrganizations: Organization[];
 }
 
 const mapStateToProps = (state: any): StateProps => ({
-  currentUser: getCurrentUser(state)
+  currentUser: getCurrentUser(state),
+  userOrganizations: getMyOrganizations(state)
 });
 
 const fetchIssueOrganizations = (organizationKeys: string[]) => (dispatch: Dispatch<any>) => {
@@ -83,6 +89,7 @@ const mapDispatchToProps = { fetchIssues: fetchIssues as any } as DispatchProps;
 
 interface OwnProps {
   location: { pathname: string; query: RawQuery };
+  hideAuthorFacet?: boolean;
   myIssues?: boolean;
 }
 
index ace5779f36b73d565a2e04ab7cc45d79923e7b13..d888dbbb50751b0d070e59437f15965724170baf 100644 (file)
@@ -46,6 +46,7 @@ import { Component } from '../../../app/types';
 export interface Props {
   component: Component | undefined;
   facets: { [facet: string]: Facet };
+  hideAuthorFacet?: boolean;
   loading?: boolean;
   loadingFacets: { [key: string]: boolean };
   myIssues: boolean;
@@ -62,14 +63,14 @@ export interface Props {
 
 export default class Sidebar extends React.PureComponent<Props> {
   render() {
-    const { component, facets, openFacets, query } = this.props;
+    const { component, facets, hideAuthorFacet, openFacets, query } = this.props;
 
     const displayProjectsFacet =
       !component || !['TRK', 'BRC', 'DIR', 'DEV_PRJ'].includes(component.qualifier);
     const displayModulesFacet = component !== undefined && component.qualifier !== 'DIR';
     const displayDirectoriesFacet = component !== undefined && component.qualifier !== 'DIR';
     const displayFilesFacet = component !== undefined;
-    const displayAuthorFacet = !component || component.qualifier !== 'DEV';
+    const displayAuthorFacet = !hideAuthorFacet && (!component || component.qualifier !== 'DEV');
 
     const organizationKey =
       (component && component.organization) ||
index b9d98b78d9a2ff00afd37015b5168c9a1f8854a0..fbbbf4dafae228266980857bf4782f98e8f3d088 100644 (file)
@@ -158,6 +158,10 @@ export function mapFacet(facet: string) {
 }
 
 export function parseFacets(facets: RawFacet[]) {
+  if (!facets) {
+    return {};
+  }
+
   // for readability purpose
   const propertyMapping: { [x: string]: string } = {
     fileUuids: 'files',
index 83c326a12ea49d056c8d058b0d5c44d6443aa18b..394c321b12d35713ad91587fe39d63d0fbf768da 100644 (file)
@@ -49,7 +49,7 @@ export default class LineSCM extends React.PureComponent<Props> {
     const { line, popupOpen, previousLine } = this.props;
     const hasPopup = !!line.line;
     const cell = isSCMChanged(line, previousLine) && (
-      <div className="source-line-scm-inner" data-author={line.scmAuthor} />
+      <div className="source-line-scm-inner" data-author={line.scmAuthor || '…'} />
     );
     return hasPopup ? (
       <td
@@ -76,8 +76,8 @@ export default class LineSCM extends React.PureComponent<Props> {
 
 function isSCMChanged(s: SourceLine, p: SourceLine | undefined) {
   let changed = true;
-  if (p != null && s.scmAuthor != null && p.scmAuthor != null) {
-    changed = s.scmAuthor !== p.scmAuthor || s.scmDate !== p.scmDate;
+  if (p != null && s.scmRevision != null && p.scmRevision != null) {
+    changed = s.scmRevision !== p.scmRevision || s.scmDate !== p.scmDate;
   }
   return changed;
 }
index fa1b766c12a6fa166cd42a9d2cf54b1863f22ffe..52f78a7382b71a0e365232a7866063d035352fec 100644 (file)
@@ -18,6 +18,7 @@
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 import * as React from 'react';
+import * as classNames from 'classnames';
 import { SourceLine } from '../../../app/types';
 import { DropdownOverlay } from '../../controls/Dropdown';
 import DateFormatter from '../../intl/DateFormatter';
@@ -28,12 +29,13 @@ interface Props {
 }
 
 export default function SCMPopup({ line }: Props) {
+  const hasAuthor = line.scmAuthor !== '';
   return (
     <DropdownOverlay placement={PopupPlacement.RightTop}>
       <div className="source-viewer-bubble-popup abs-width-400">
-        <div>{line.scmAuthor}</div>
+        {hasAuthor && <div>{line.scmAuthor}</div>}
         {line.scmDate && (
-          <div className="spacer-top">
+          <div className={classNames({ 'spacer-top': hasAuthor })}>
             <DateFormatter date={line.scmDate} />
           </div>
         )}
index 29e82eb3d6129b11ca9a35870be8d61eaf0ce2f3..343f9350aad5236905fdf8e439c61e6f5a0438e8 100644 (file)
@@ -32,7 +32,7 @@ it('render scm details', () => {
 });
 
 it('render scm details for the first line', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
   const wrapper = shallow(
     <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={undefined} />
   );
@@ -40,8 +40,17 @@ it('render scm details for the first line', () => {
 });
 
 it('does not render scm details', () => {
-  const line = { line: 3, scmAuthor: 'foo', scmDate: '2017-01-01' };
-  const previousLine = { line: 2, scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const line = { line: 3, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmRevision: 'foo', scmAuthor: 'foo', scmDate: '2017-01-01' };
+  const wrapper = shallow(
+    <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
+  );
+  expect(wrapper).toMatchSnapshot();
+});
+
+it('renders ellipsis when no author info', () => {
+  const line = { line: 3, scmRevision: 'foo', scmDate: '2017-01-01' };
+  const previousLine = { line: 2, scmRevision: 'bar', scmDate: '2017-01-01' };
   const wrapper = shallow(
     <LineSCM line={line} onPopupToggle={jest.fn()} popupOpen={false} previousLine={previousLine} />
   );
index dac002460eb90f35bb93bd4f5795bfe6ba83e4c4..b72bebfac307958e5657f998a4201aab6f898c93 100644 (file)
@@ -18,6 +18,7 @@ exports[`does not render scm details 1`] = `
             "line": 3,
             "scmAuthor": "foo",
             "scmDate": "2017-01-01",
+            "scmRevision": "foo",
           }
         }
       />
@@ -75,6 +76,7 @@ exports[`render scm details for the first line 1`] = `
             "line": 3,
             "scmAuthor": "foo",
             "scmDate": "2017-01-01",
+            "scmRevision": "foo",
           }
         }
       />
@@ -87,3 +89,34 @@ exports[`render scm details for the first line 1`] = `
   </Toggler>
 </td>
 `;
+
+exports[`renders ellipsis when no author info 1`] = `
+<td
+  className="source-meta source-line-scm"
+  data-line-number={3}
+  onClick={[Function]}
+  role="button"
+  tabIndex={0}
+>
+  <Toggler
+    onRequestClose={[Function]}
+    open={false}
+    overlay={
+      <SCMPopup
+        line={
+          Object {
+            "line": 3,
+            "scmDate": "2017-01-01",
+            "scmRevision": "foo",
+          }
+        }
+      />
+    }
+  >
+    <div
+      className="source-line-scm-inner"
+      data-author="…"
+    />
+  </Toggler>
+</td>
+`;