瀏覽代碼

MMF-769 User can close their account (#1861)

tags/8.0
Benoit 4 年之前
父節點
當前提交
7f1afd8ce4
共有 100 個檔案被更改,包括 2281 行新增805 行删除
  1. 0
    1
      server/sonar-db-core/src/test/java/org/sonar/db/DefaultOrganizationTesting.java
  2. 0
    14
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationDto.java
  3. 47
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationHelper.java
  4. 0
    42
      server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java
  5. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java
  6. 0
    11
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDto.java
  7. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java
  8. 0
    27
      server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml
  9. 14
    6
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml
  10. 4
    65
      server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java
  11. 141
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationHelperTest.java
  12. 0
    67
      server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java
  13. 39
    10
      server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
  14. 1
    1
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateRulesMetadataTest/rules_and_rules_metadata_and_organization_and_internal_properties.sql
  15. 21
    8
      server/sonar-docs/src/pages/sonarcloud/organizations/index.md
  16. 33
    4
      server/sonar-docs/src/pages/user-guide/user-account.md
  17. 1
    0
      server/sonar-docs/static/SonarCloudNavigationTree.json
  18. 0
    5
      server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java
  19. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java
  20. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdater.java
  21. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java
  22. 0
    77
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java
  23. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java
  24. 87
    0
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/PreventUserDeletionAction.java
  25. 2
    2
      server/sonar-server/src/main/java/org/sonar/server/organization/ws/SearchMembersAction.java
  26. 1
    1
      server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java
  27. 0
    5
      server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java
  28. 0
    5
      server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java
  29. 0
    5
      server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java
  30. 0
    5
      server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java
  31. 6
    1
      server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java
  32. 1
    2
      server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java
  33. 35
    29
      server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
  34. 1
    14
      server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateLoginAction.java
  35. 2
    1
      server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java
  36. 1
    0
      server/sonar-server/src/main/resources/org/sonar/server/issue/ws/changelog-example.json
  37. 12
    0
      server/sonar-server/src/main/resources/org/sonar/server/organization/ws/prevent_user_deletion-example.json
  38. 20
    0
      server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java
  39. 1
    6
      server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationUpdaterImplTest.java
  40. 0
    141
      server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java
  41. 208
    0
      server/sonar-server/src/test/java/org/sonar/server/organization/ws/PreventUserDeletionActionTest.java
  42. 2
    20
      server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchActionTest.java
  43. 0
    5
      server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java
  44. 0
    4
      server/sonar-server/src/test/java/org/sonar/server/tester/MockUserSession.java
  45. 0
    5
      server/sonar-server/src/test/java/org/sonar/server/tester/UserSessionRule.java
  46. 2
    2
      server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java
  47. 0
    3
      server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java
  48. 0
    5
      server/sonar-server/src/test/java/org/sonar/server/user/TestUserSessionFactory.java
  49. 48
    8
      server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
  50. 0
    18
      server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateLoginActionTest.java
  51. 1
    1
      server/sonar-web/package.json
  52. 5
    9
      server/sonar-web/src/main/js/api/issues.ts
  53. 6
    0
      server/sonar-web/src/main/js/api/organizations.ts
  54. 2
    7
      server/sonar-web/src/main/js/api/quality-profiles.ts
  55. 1
    7
      server/sonar-web/src/main/js/api/user_groups.ts
  56. 53
    0
      server/sonar-web/src/main/js/app/components/AccountDeleted.tsx
  57. 27
    0
      server/sonar-web/src/main/js/app/components/__tests__/AccountDeleted-test.tsx
  58. 53
    0
      server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AccountDeleted-test.tsx.snap
  59. 4
    0
      server/sonar-web/src/main/js/app/styles/init/lists.css
  60. 39
    27
      server/sonar-web/src/main/js/app/types.d.ts
  61. 5
    1
      server/sonar-web/src/main/js/app/utils/startReactApp.tsx
  62. 9
    0
      server/sonar-web/src/main/js/apps/account/profile/Profile.tsx
  63. 125
    0
      server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx
  64. 148
    0
      server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx
  65. 150
    0
      server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx
  66. 87
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx
  67. 71
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx
  68. 86
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx
  69. 118
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap
  70. 128
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap
  71. 19
    0
      server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap
  72. 7
    2
      server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx
  73. 13
    5
      server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx
  74. 87
    0
      server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap
  75. 2
    7
      server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx
  76. 2
    3
      server/sonar-web/src/main/js/apps/issues/components/App.tsx
  77. 6
    2
      server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx
  78. 31
    20
      server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx
  79. 2
    9
      server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx
  80. 38
    7
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx
  81. 56
    0
      server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap
  82. 1
    12
      server/sonar-web/src/main/js/apps/issues/utils.ts
  83. 19
    14
      server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx
  84. 8
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx
  85. 35
    1
      server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap
  86. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/App.tsx
  87. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx
  88. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx
  89. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx
  90. 1
    1
      server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx
  91. 3
    9
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx
  92. 7
    7
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx
  93. 4
    4
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx
  94. 2
    3
      server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx
  95. 7
    9
      server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx
  96. 2
    2
      server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx
  97. 50
    0
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx
  98. 14
    0
      server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap
  99. 1
    1
      server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx
  100. 0
    0
      server/sonar-web/src/main/js/apps/users/components/UserActions.tsx

+ 0
- 1
server/sonar-db-core/src/test/java/org/sonar/db/DefaultOrganizationTesting.java 查看文件

@@ -46,7 +46,6 @@ public class DefaultOrganizationTesting {
"UUID", uuid,
"KEE", uuid,
"NAME", uuid,
"GUARDED", String.valueOf(false),
"CREATED_AT", "1000",
"UPDATED_AT", "1000");
}

+ 0
- 14
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationDto.java 查看文件

@@ -73,10 +73,6 @@ public class OrganizationDto {

private Subscription subscription;

/**
* Flag indicated whether being root is required to be able to delete this organization.
*/
private boolean guarded = false;
private Integer defaultGroupId;
private String defaultQualityGateUuid;
private long createdAt;
@@ -139,15 +135,6 @@ public class OrganizationDto {
return this;
}

public boolean isGuarded() {
return guarded;
}

public OrganizationDto setGuarded(boolean guarded) {
this.guarded = guarded;
return this;
}

@CheckForNull
public Integer getDefaultGroupId() {
return defaultGroupId;
@@ -220,7 +207,6 @@ public class OrganizationDto {
", description='" + description + '\'' +
", url='" + url + '\'' +
", avatarUrl='" + avatarUrl + '\'' +
", guarded=" + guarded +
", defaultQualityGateUuid=" + defaultQualityGateUuid +
", subscription=" + subscription +
", createdAt=" + createdAt +

+ 47
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationHelper.java 查看文件

@@ -0,0 +1,47 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.db.organization;

import java.util.List;
import java.util.stream.Collectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.permission.OrganizationPermission;

public class OrganizationHelper {

private static final String ADMIN_PERMISSION = OrganizationPermission.ADMINISTER.getKey();

private final DbClient dbClient;

public OrganizationHelper(DbClient dbClient) {
this.dbClient = dbClient;
}

public List<OrganizationDto> selectOrganizationsWithLastAdmin(DbSession dbSession, int userId) {
return dbClient.organizationDao().selectByPermission(dbSession, userId, ADMIN_PERMISSION).stream()
.filter(org -> isLastAdmin(dbSession, org, userId))
.collect(Collectors.toList());
}

private boolean isLastAdmin(DbSession dbSession, OrganizationDto org, int userId) {
return dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingUser(dbSession, org.getUuid(), ADMIN_PERMISSION, userId) == 0;
}
}

+ 0
- 42
server/sonar-db-dao/src/main/java/org/sonar/db/organization/OrganizationQuery.java 查看文件

@@ -32,27 +32,15 @@ public class OrganizationQuery {
private final Set<String> keys;
@Nullable
private final Integer userId;
private final boolean onlyTeam;
private final boolean onlyPersonal;
private final boolean withAnalyses;
private final boolean withoutProjects;
@Nullable
private final Long analyzedAfter;

private OrganizationQuery(Builder builder) {
this.keys = builder.keys;
this.userId = builder.member;
this.onlyPersonal = builder.onlyPersonal;
this.onlyTeam = builder.onlyTeam;
if (this.onlyPersonal && this.onlyTeam) {
throw new IllegalArgumentException("Only one of onlyPersonal and onlyTeam can be true");
}
this.withAnalyses = builder.withAnalyses;
this.analyzedAfter = builder.analyzedAfter;
this.withoutProjects = builder.withoutProjects;
if ((this.withAnalyses || this.analyzedAfter != null) && this.withoutProjects) {
throw new IllegalArgumentException("withoutProjects cannot be used together with withAnalyses or analyzedAfter");
}
}

@CheckForNull
@@ -65,14 +53,6 @@ public class OrganizationQuery {
return userId;
}

public boolean isOnlyTeam() {
return onlyTeam;
}

public boolean isOnlyPersonal() {
return onlyPersonal;
}

public boolean isWithAnalyses() {
return withAnalyses;
}
@@ -82,10 +62,6 @@ public class OrganizationQuery {
return analyzedAfter;
}

public boolean isWithoutProjects() {
return withoutProjects;
}

public static OrganizationQuery returnAll() {
return NO_FILTER;
}
@@ -98,10 +74,7 @@ public class OrganizationQuery {
private Set<String> keys;
@Nullable
private Integer member;
private boolean onlyTeam = false;
private boolean onlyPersonal = false;
private boolean withAnalyses = false;
private boolean withoutProjects = false;
@Nullable
private Long analyzedAfter;

@@ -123,16 +96,6 @@ public class OrganizationQuery {
return this;
}

public Builder setOnlyTeam() {
this.onlyTeam = true;
return this;
}

public Builder setOnlyPersonal() {
this.onlyPersonal = true;
return this;
}

public Builder setWithAnalyses() {
this.withAnalyses = true;
return this;
@@ -143,11 +106,6 @@ public class OrganizationQuery {
return this;
}

public Builder setWithoutProjects() {
this.withoutProjects = true;
return this;
}

public OrganizationQuery build() {
return new OrganizationQuery(this);
}

+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDao.java 查看文件

@@ -135,6 +135,10 @@ public class UserDao implements Dao {
mapper(dbSession).deactivateUser(user.getLogin(), system2.now());
}

public void deactivateSonarCloudUser(DbSession dbSession, UserDto user) {
mapper(dbSession).deactivateSonarCloudUser(user.getLogin(), system2.now());
}

public void cleanHomepage(DbSession dbSession, OrganizationDto organization) {
mapper(dbSession).clearHomepages("ORGANIZATION", organization.getUuid(), system2.now());
}

+ 0
- 11
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDto.java 查看文件

@@ -55,7 +55,6 @@ public class UserDto {
private boolean local = true;
private boolean root = false;
private boolean onboarded = false;
private String organizationUuid;

/**
* Date of the last time the user has accessed to the server.
@@ -272,16 +271,6 @@ public class UserDto {
return this;
}

@CheckForNull
public String getOrganizationUuid() {
return organizationUuid;
}

public UserDto setOrganizationUuid(@Nullable String organizationUuid) {
this.organizationUuid = organizationUuid;
return this;
}

@CheckForNull
public Long getLastConnectionDate() {
return lastConnectionDate;

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserMapper.java 查看文件

@@ -82,6 +82,8 @@ public interface UserMapper {

void deactivateUser(@Param("login") String login, @Param("now") long now);

void deactivateSonarCloudUser(@Param("login") String login, @Param("now") long now);

void clearHomepages(@Param("homepageType") String type, @Param("homepageParameter") String value, @Param("now") long now);

void clearHomepage(@Param("login") String login, @Param("now") long now);

+ 0
- 27
server/sonar-db-dao/src/main/resources/org/sonar/db/organization/OrganizationMapper.xml 查看文件

@@ -10,7 +10,6 @@
org.default_quality_gate_uuid as "defaultQualityGateUuid",
org.url as "url",
org.avatar_url as "avatarUrl",
org.guarded as "guarded",
org.subscription as "subscription",
org.created_at as "createdAt",
org.updated_at as "updatedAt"
@@ -121,22 +120,6 @@
#{key, jdbcType=VARCHAR}
</foreach>
</if>
<if test="query.onlyTeam">
and not exists(
select 1
from users u
where u.organization_uuid = org.uuid
and u.active = ${_true}
)
</if>
<if test="query.onlyPersonal">
and exists(
select 1
from users u
where u.organization_uuid = org.uuid
and u.active = ${_true}
)
</if>
<if test="query.withAnalyses">
and exists(
select 1
@@ -147,14 +130,6 @@
and s.islast = ${_true}
)
</if>
<if test="query.withoutProjects">
and not exists(
select 1
from projects p
where p.organization_uuid = org.uuid
and p.enabled = ${_true}
)
</if>
<if test="query.analyzedAfter != null">
and exists(
select 1
@@ -246,7 +221,6 @@
description,
url,
avatar_url,
guarded,
new_project_private,
default_quality_gate_uuid,
subscription,
@@ -261,7 +235,6 @@
#{organization.description, jdbcType=VARCHAR},
#{organization.url, jdbcType=VARCHAR},
#{organization.avatarUrl, jdbcType=VARCHAR},
#{organization.guarded, jdbcType=BOOLEAN},
#{newProjectPrivate, jdbcType=BOOLEAN},
#{organization.defaultQualityGateUuid, jdbcType=VARCHAR},
#{organization.subscription, jdbcType=VARCHAR},

+ 14
- 6
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserMapper.xml 查看文件

@@ -22,7 +22,6 @@
u.onboarded as "onboarded",
u.homepage_type as "homepageType",
u.homepage_parameter as "homepageParameter",
u.organization_uuid as organizationUuid,
u.last_connection_date as "lastConnectionDate",
u.created_at as "createdAt",
u.updated_at as "updatedAt"
@@ -173,8 +172,7 @@
and u.login &lt;&gt; #{login}
</select>

<update id="deactivateUser" parameterType="map">
update users set
<sql id="deactivateUserUpdatedFields">
active = ${_false},
email = null,
scm_accounts = null,
@@ -182,6 +180,19 @@
crypted_password = null,
last_connection_date = null,
updated_at = #{now, jdbcType=BIGINT}
</sql>

<update id="deactivateUser" parameterType="map">
update users set
<include refid="deactivateUserUpdatedFields"/>
where
login = #{login, jdbcType=VARCHAR}
</update>

<update id="deactivateSonarCloudUser" parameterType="map">
update users set
name = null,
<include refid="deactivateUserUpdatedFields"/>
where
login = #{login, jdbcType=VARCHAR}
</update>
@@ -233,7 +244,6 @@
onboarded,
homepage_type,
homepage_parameter,
organization_uuid,
created_at,
updated_at
) values (
@@ -254,7 +264,6 @@
#{user.onboarded,jdbcType=BOOLEAN},
#{user.homepageType,jdbcType=VARCHAR},
#{user.homepageParameter,jdbcType=VARCHAR},
#{user.organizationUuid,jdbcType=VARCHAR},
#{user.createdAt,jdbcType=BIGINT},
#{user.updatedAt,jdbcType=BIGINT}
)
@@ -277,7 +286,6 @@
hash_method = #{user.hashMethod, jdbcType=VARCHAR},
homepage_type = #{user.homepageType, jdbcType=VARCHAR},
homepage_parameter = #{user.homepageParameter, jdbcType=VARCHAR},
organization_uuid = #{user.organizationUuid, jdbcType=VARCHAR},
last_connection_date = #{user.lastConnectionDate,jdbcType=BIGINT},
updated_at = #{user.updatedAt,jdbcType=BIGINT}
where

+ 4
- 65
server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationDaoTest.java 查看文件

@@ -50,8 +50,6 @@ import org.sonar.db.Pagination;
import org.sonar.db.alm.ALM;
import org.sonar.db.alm.AlmAppInstallDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.dialect.Dialect;
import org.sonar.db.dialect.Oracle;
import org.sonar.db.metric.MetricDto;
import org.sonar.db.qualitygate.QGateWithOrgDto;
import org.sonar.db.user.GroupDto;
@@ -86,7 +84,6 @@ public class OrganizationDaoTest {
.setDescription("the description 1")
.setUrl("the url 1")
.setAvatarUrl("the avatar url 1")
.setGuarded(false)
.setSubscription(FREE)
.setDefaultQualityGateUuid("1");
private static final OrganizationDto ORGANIZATION_DTO_2 = new OrganizationDto()
@@ -96,7 +93,6 @@ public class OrganizationDaoTest {
.setDescription("the description 2")
.setUrl("the url 2")
.setAvatarUrl("the avatar url 2")
.setGuarded(true)
.setSubscription(FREE)
.setDefaultQualityGateUuid("1");
private static final String PERMISSION_1 = "foo";
@@ -148,21 +144,12 @@ public class OrganizationDaoTest {
assertThat(row.get("avatarUrl")).isEqualTo(organization.getAvatarUrl());
assertThat(row.get("createdAt")).isEqualTo(organization.getCreatedAt());
assertThat(row.get("updatedAt")).isEqualTo(organization.getUpdatedAt());
assertThat(row.get("guarded")).isEqualTo(toBool(organization.isGuarded()));
assertThat(row.get("subscription")).isEqualTo(organization.getSubscription().name());
assertThat(row.get("defaultTemplate")).isNull();
assertThat(row.get("projectDefaultTemplate")).isNull();
assertThat(row.get("viewDefaultTemplate")).isNull();
}

@Test
public void insert_persists_boolean_property_guarded_of_OrganizationDto() {
insertOrganization(ORGANIZATION_DTO_2);

Map<String, Object> row = selectSingleRow();
assertThat(row.get("guarded")).isEqualTo(toBool(ORGANIZATION_DTO_2.isGuarded()));
}

@Test
public void description_url_avatarUrl_and_userId_are_optional() {
when(system2.now()).thenReturn(SOME_DATE);
@@ -175,7 +162,6 @@ public class OrganizationDaoTest {
assertThat(row.get("description")).isNull();
assertThat(row.get("url")).isNull();
assertThat(row.get("avatarUrl")).isNull();
assertThat(row.get("guarded")).isEqualTo(toBool(ORGANIZATION_DTO_1.isGuarded()));
assertThat(row.get("userId")).isNull();
assertThat(row.get("createdAt")).isEqualTo(SOME_DATE);
assertThat(row.get("updatedAt")).isEqualTo(SOME_DATE);
@@ -184,14 +170,6 @@ public class OrganizationDaoTest {
assertThat(row.get("viewDefaultTemplate")).isNull();
}

private Object toBool(boolean guarded) {
Dialect dialect = db.database().getDialect();
if (dialect.getId().equals(Oracle.ID)) {
return guarded ? 1L : 0L;
}
return guarded;
}

@Test
public void insert_fails_if_row_with_uuid_already_exists() {
insertOrganization(ORGANIZATION_DTO_1);
@@ -584,20 +562,6 @@ public class OrganizationDaoTest {
.doesNotContain(organizationWithoutKeyProvided.getUuid(), organizationWithoutMember.getUuid());
}

@Test
public void selectByQuery_filter_on_type() {
OrganizationDto personalOrg1 = db.organizations().insert();
db.users().insertUser(u -> u.setOrganizationUuid(personalOrg1.getUuid()));
OrganizationDto personalOrg2 = db.organizations().insert();
db.users().insertUser(u -> u.setOrganizationUuid(personalOrg2.getUuid()));
OrganizationDto teamOrg1 = db.organizations().insert();

assertThat(selectUuidsByQuery(q -> q.setOnlyPersonal(), forPage(1).andSize(100)))
.containsExactlyInAnyOrder(personalOrg1.getUuid(), personalOrg2.getUuid());
assertThat(selectUuidsByQuery(q -> q.setOnlyTeam(), forPage(1).andSize(100)))
.containsExactlyInAnyOrder(teamOrg1.getUuid());
}

@Test
public void selectByQuery_filter_on_withAnalyses() {
assertThat(selectUuidsByQuery(q -> q.setWithAnalyses(), forPage(1).andSize(100)))
@@ -629,25 +593,6 @@ public class OrganizationDaoTest {
.collect(Collectors.toList());
}

@Test
public void selectByQuery_filter_on_withoutProjects() {
assertThat(selectUuidsByQuery(q -> q.setWithoutProjects(), forPage(1).andSize(100)))
.isEmpty();

// has projects
OrganizationDto orgWithProjects = db.organizations().insert();
db.components().insertPrivateProject(orgWithProjects);
db.components().insertPrivateProject(orgWithProjects, p -> p.setEnabled(false));
// has no projects
OrganizationDto orgWithoutProjects = db.organizations().insert();
// has only disabled projects
OrganizationDto orgWithOnlyDisabledProjects = db.organizations().insert();
db.components().insertPrivateProject(orgWithOnlyDisabledProjects, p -> p.setEnabled(false));

assertThat(selectUuidsByQuery(q -> q.setWithoutProjects(), forPage(1).andSize(100)))
.containsExactlyInAnyOrder(orgWithoutProjects.getUuid(), orgWithOnlyDisabledProjects.getUuid());
}

@Test
public void getDefaultTemplates_returns_empty_when_table_is_empty() {
assertThat(underTest.getDefaultTemplates(dbSession, ORGANIZATION_DTO_1.getUuid())).isEmpty();
@@ -1155,7 +1100,6 @@ public class OrganizationDaoTest {
" default_perm_template_app," +
" default_perm_template_port," +
" new_project_private," +
" guarded," +
" default_quality_gate_uuid," +
" subscription," +
" created_at," +
@@ -1173,7 +1117,6 @@ public class OrganizationDaoTest {
" ?," +
" ?," +
" ?," +
" ?," +
" ?" +
" )")) {
preparedStatement.setString(1, organizationUuid);
@@ -1183,11 +1126,10 @@ public class OrganizationDaoTest {
preparedStatement.setString(5, view);
preparedStatement.setString(6, view);
preparedStatement.setBoolean(7, false);
preparedStatement.setBoolean(8, false);
preparedStatement.setString(9, "1");
preparedStatement.setString(10, FREE.name());
preparedStatement.setLong(11, 1000L);
preparedStatement.setLong(12, 2000L);
preparedStatement.setString(8, "1");
preparedStatement.setString(9, FREE.name());
preparedStatement.setLong(10, 1000L);
preparedStatement.setLong(11, 2000L);
preparedStatement.execute();
} catch (SQLException e) {
throw new RuntimeException("dirty insert failed", e);
@@ -1210,7 +1152,6 @@ public class OrganizationDaoTest {
assertThat(dto.getName()).isEqualTo(ORGANIZATION_DTO_1.getName());
assertThat(dto.getDescription()).isEqualTo(ORGANIZATION_DTO_1.getDescription());
assertThat(dto.getUrl()).isEqualTo(ORGANIZATION_DTO_1.getUrl());
assertThat(dto.isGuarded()).isEqualTo(ORGANIZATION_DTO_1.isGuarded());
assertThat(dto.getAvatarUrl()).isEqualTo(ORGANIZATION_DTO_1.getAvatarUrl());
assertThat(dto.getCreatedAt()).isEqualTo(ORGANIZATION_DTO_1.getCreatedAt());
assertThat(dto.getUpdatedAt()).isEqualTo(ORGANIZATION_DTO_1.getUpdatedAt());
@@ -1222,7 +1163,6 @@ public class OrganizationDaoTest {
assertThat(dto.getName()).isEqualTo(expected.getName());
assertThat(dto.getDescription()).isEqualTo(expected.getDescription());
assertThat(dto.getUrl()).isEqualTo(expected.getUrl());
assertThat(dto.isGuarded()).isEqualTo(expected.isGuarded());
assertThat(dto.getAvatarUrl()).isEqualTo(expected.getAvatarUrl());
assertThat(dto.getSubscription()).isEqualTo(expected.getSubscription());
assertThat(dto.getCreatedAt()).isEqualTo(expected.getCreatedAt());
@@ -1232,7 +1172,6 @@ public class OrganizationDaoTest {
private Map<String, Object> selectSingleRow() {
List<Map<String, Object>> rows = db.select("select" +
" uuid as \"uuid\", kee as \"key\", name as \"name\", description as \"description\", url as \"url\", avatar_url as \"avatarUrl\"," +
" guarded as \"guarded\"," +
" subscription as \"subscription\"," +
" created_at as \"createdAt\", updated_at as \"updatedAt\"," +
" default_perm_template_project as \"projectDefaultPermTemplate\"," +

+ 141
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationHelperTest.java 查看文件

@@ -0,0 +1,141 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.db.organization;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.List;
import java.util.Random;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.sonar.api.utils.System2;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;

@RunWith(DataProviderRunner.class)
public class OrganizationHelperTest {

private static final Random RANDOM = new Random();

@Rule
public DbTester db = DbTester.create(mock(System2.class)).setDisableDefaultOrganization(true);
public DbSession dbSession = db.getSession();

@Rule
public ExpectedException expectedException = ExpectedException.none();

private final OrganizationHelper underTest = new OrganizationHelper(db.getDbClient());

@Test
public void returns_empty_list_when_user_is_not_admin_of_any_orgs() {
UserDto user1 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert();
GroupDto group1 = db.users().insertGroup(org1);
db.users().insertMember(group1, user1);

assertThat(underTest.selectOrganizationsWithLastAdmin(dbSession, user1.getId())).isEmpty();
}

@Test
public void returns_orgs_where_user_is_last_admin() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert();
OrganizationDto org2 = db.organizations().insert();

setAsDirectOrIndirectAdmin(user1, org1);
setAsDirectOrIndirectAdmin(user2, org1);
setAsDirectOrIndirectAdmin(user1, org2);

assertThat(underTest.selectOrganizationsWithLastAdmin(dbSession, user1.getId()))
.extracting(OrganizationDto::getKey)
.containsExactly(org2.getKey());
}

@Test
@UseDataProvider("adminUserCombinationsAndExpectedOrgKeys")
public void returns_correct_orgs_for_interesting_combinations_of_last_admin_or_not(
boolean user2IsAdminOfOrg1, boolean user1IsAdminOfOrg2, boolean user2IsAdminOfOrg2, List<String> expectedOrgKeys) {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert(o -> o.setKey("org1"));
OrganizationDto org2 = db.organizations().insert(o -> o.setKey("org2"));

setAsDirectOrIndirectAdmin(user1, org1);
if (user2IsAdminOfOrg1) {
setAsDirectOrIndirectAdmin(user2, org1);
}
if (user1IsAdminOfOrg2) {
setAsDirectOrIndirectAdmin(user1, org2);
}
if (user2IsAdminOfOrg2) {
setAsDirectOrIndirectAdmin(user2, org2);
}

assertThat(underTest.selectOrganizationsWithLastAdmin(dbSession, user1.getId()))
.extracting(OrganizationDto::getKey)
.containsExactlyInAnyOrderElementsOf(expectedOrgKeys);
}

@DataProvider
public static Object[][] adminUserCombinationsAndExpectedOrgKeys() {
return new Object[][] {
// note: user1 is always admin of org1
// param 1: user2 is admin of org1
// param 2: user1 is admin of org2
// param 3: user2 is admin of org2
// param 4: list of orgs preventing user1 to delete
{true, true, true, emptyList()},
{true, true, false, singletonList("org2")},
{true, false, true, emptyList()},
{true, false, false, emptyList()},
{false, true, true, singletonList("org1")},
{false, true, false, asList("org1", "org2")},
{false, false, true, singletonList("org1")},
{false, false, false, singletonList("org1")},
};
}

private void setAsDirectOrIndirectAdmin(UserDto user, OrganizationDto organization) {
boolean useDirectAdmin = RANDOM.nextBoolean();
if (useDirectAdmin) {
db.users().insertPermissionOnUser(organization, user, ADMINISTER);
} else {
GroupDto group = db.users().insertGroup(organization);
db.users().insertPermissionOnGroup(group, ADMINISTER);
db.users().insertMember(group, user);
}
}
}

+ 0
- 67
server/sonar-db-dao/src/test/java/org/sonar/db/organization/OrganizationQueryTest.java 查看文件

@@ -1,67 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.db.organization;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class OrganizationQueryTest {

@Rule
public ExpectedException expectedException = ExpectedException.none();

@Test
public void throws_IAE_when_both_onlyPersonal_and_onlyTeam_are_set() {
expectedException.expect(IllegalArgumentException.class);
OrganizationQuery.newOrganizationQueryBuilder()
.setOnlyPersonal()
.setOnlyTeam()
.build();
}

@Test
public void throws_IAE_when_withoutProjects_is_used_together_with_withAnalyses() {
expectedException.expect(IllegalArgumentException.class);
OrganizationQuery.newOrganizationQueryBuilder()
.setWithoutProjects()
.setWithAnalyses()
.build();
}

@Test
public void throws_IAE_when_withoutProjects_is_used_together_with_analyzedAfter() {
expectedException.expect(IllegalArgumentException.class);
OrganizationQuery.newOrganizationQueryBuilder()
.setWithoutProjects()
.setAnalyzedAfter(1)
.build();
}

@Test
public void throws_IAE_when_withoutProjects_is_used_together_with_both_withAnalyses_and_analyzedAfter() {
expectedException.expect(IllegalArgumentException.class);
OrganizationQuery.newOrganizationQueryBuilder()
.setWithoutProjects()
.setWithAnalyses()
.setAnalyzedAfter(1)
.build();
}
}

+ 39
- 10
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java 查看文件

@@ -19,12 +19,15 @@
*/
package org.sonar.db.user;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.user.UserQuery;
import org.sonar.api.utils.DateUtils;
import org.sonar.api.impl.utils.TestSystem2;
@@ -44,6 +47,7 @@ import static org.assertj.core.groups.Tuple.tuple;
import static org.sonar.db.user.GroupTesting.newGroupDto;
import static org.sonar.db.user.UserTesting.newUserDto;

@RunWith(DataProviderRunner.class)
public class UserDaoTest {
private static final long NOW = 1_500_000_000_000L;

@@ -336,7 +340,6 @@ public class UserDaoTest {
.setLocal(true)
.setHomepageType("project")
.setHomepageParameter("OB1")
.setOrganizationUuid("ORG_UUID")
.setCreatedAt(date)
.setUpdatedAt(date);
underTest.insert(db.getSession(), userDto);
@@ -361,7 +364,6 @@ public class UserDaoTest {
assertThat(user.isRoot()).isFalse();
assertThat(user.getHomepageType()).isEqualTo("project");
assertThat(user.getHomepageParameter()).isEqualTo("OB1");
assertThat(user.getOrganizationUuid()).isEqualTo("ORG_UUID");
}

@Test
@@ -383,8 +385,7 @@ public class UserDaoTest {
.setEmail("jo@hn.com")
.setActive(true)
.setLocal(true)
.setOnboarded(false)
.setOrganizationUuid("OLD_ORG_UUID"));
.setOnboarded(false));

underTest.update(db.getSession(), newUserDto()
.setUuid(user.getUuid())
@@ -403,7 +404,6 @@ public class UserDaoTest {
.setLocal(false)
.setHomepageType("project")
.setHomepageParameter("OB1")
.setOrganizationUuid("ORG_UUID")
.setLastConnectionDate(10_000_000_000L));

UserDto reloaded = underTest.selectByUuid(db.getSession(), user.getUuid());
@@ -425,7 +425,6 @@ public class UserDaoTest {
assertThat(reloaded.isRoot()).isFalse();
assertThat(reloaded.getHomepageType()).isEqualTo("project");
assertThat(reloaded.getHomepageParameter()).isEqualTo("OB1");
assertThat(reloaded.getOrganizationUuid()).isEqualTo("ORG_UUID");
assertThat(reloaded.getLastConnectionDate()).isEqualTo(10_000_000_000L);
}

@@ -441,10 +440,40 @@ public class UserDaoTest {

UserDto userReloaded = underTest.selectUserById(session, user.getId());
assertThat(userReloaded.isActive()).isFalse();
assertThat(userReloaded.getLogin()).isNotNull();
assertThat(userReloaded.getExternalId()).isNotNull();
assertThat(userReloaded.getExternalLogin()).isNotNull();
assertThat(userReloaded.getExternalIdentityProvider()).isNotNull();
assertThat(userReloaded.getName()).isEqualTo(user.getName());
assertThat(userReloaded.getLogin()).isEqualTo(user.getLogin());
assertThat(userReloaded.getExternalId()).isEqualTo(user.getExternalId());
assertThat(userReloaded.getExternalLogin()).isEqualTo(user.getExternalLogin());
assertThat(userReloaded.getExternalIdentityProvider()).isEqualTo(user.getExternalIdentityProvider());
assertThat(userReloaded.getEmail()).isNull();
assertThat(userReloaded.getScmAccounts()).isNull();
assertThat(userReloaded.getSalt()).isNull();
assertThat(userReloaded.getCryptedPassword()).isNull();
assertThat(userReloaded.isRoot()).isFalse();
assertThat(userReloaded.getUpdatedAt()).isEqualTo(NOW);
assertThat(userReloaded.getHomepageType()).isNull();
assertThat(userReloaded.getHomepageParameter()).isNull();
assertThat(userReloaded.getLastConnectionDate()).isNull();
assertThat(underTest.selectUserById(session, otherUser.getId())).isNotNull();
}

@Test
public void deactivate_sonarcloud_user() {
UserDto user = insertActiveUser();
insertUserGroup(user);
UserDto otherUser = insertActiveUser();
underTest.update(db.getSession(), user.setLastConnectionDate(10_000_000_000L));
session.commit();

underTest.deactivateSonarCloudUser(session, user);

UserDto userReloaded = underTest.selectUserById(session, user.getId());
assertThat(userReloaded.isActive()).isFalse();
assertThat(userReloaded.getName()).isNull();
assertThat(userReloaded.getLogin()).isEqualTo(user.getLogin());
assertThat(userReloaded.getExternalId()).isEqualTo(user.getExternalId());
assertThat(userReloaded.getExternalLogin()).isEqualTo(user.getExternalLogin());
assertThat(userReloaded.getExternalIdentityProvider()).isEqualTo(user.getExternalIdentityProvider());
assertThat(userReloaded.getEmail()).isNull();
assertThat(userReloaded.getScmAccounts()).isNull();
assertThat(userReloaded.getSalt()).isNull();

+ 1
- 1
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v64/PopulateRulesMetadataTest/rules_and_rules_metadata_and_organization_and_internal_properties.sql 查看文件

@@ -5,7 +5,7 @@ CREATE TABLE "ORGANIZATIONS" (
"DESCRIPTION" VARCHAR(256),
"URL" VARCHAR(256),
"AVATAR_URL" VARCHAR(256),
"GUARDED" BOOLEAN NOT NULL,
"GUARDED" BOOLEAN,
"USER_ID" INTEGER,
"DEFAULT_PERM_TEMPLATE_PROJECT" VARCHAR(40),
"DEFAULT_PERM_TEMPLATE_VIEW" VARCHAR(40),

+ 21
- 8
server/sonar-docs/src/pages/sonarcloud/organizations/index.md 查看文件

@@ -8,24 +8,28 @@ url: /organizations/overview/
An organization is a space where a team or a whole company can collaborate across many projects.

An organization consists of:
* Projects, on which users collaborate
* [Members](/organizations/manage-team/), who can have different permissions on the projects
* [Quality Profiles](/instance-administration/quality-profiles/) and [Quality Gates](/user-guide/quality-gates/), which can be customized and shared accross projects

- Projects, on which users collaborate
- [Members](/organizations/manage-team/), who can have different permissions on the projects
- [Quality Profiles](/instance-administration/quality-profiles/) and [Quality Gates](/user-guide/quality-gates/), which can be customized and shared accross projects

Organizations can be on:
* **Free plan**. This is the default plan. Every project in an organization on the free plan is public.
* **Paid plan**. This plan unlocks the ability to have private projects. Go to the "Billing" page of your organization to upgrade it to the paid plan.

- **Free plan**. This is the default plan. Every project in an organization on the free plan is public.
- **Paid plan**. This plan unlocks the ability to have private projects. Go to the "Billing" page of your organization to upgrade it to the paid plan.

Depending on which plan the organization is in, its [visibility](/organizations/organization-visibility/) will change.

You can create organizations from the top right menu "+ > Create new organization"

## How to bind an existing organization to GitHub or Bitbucket Cloud?
## FAQ

### How to bind an existing organization to GitHub or Bitbucket Cloud?

You might notice the following warning message on your pull requests inside SonarCloud:

The SonarCloud GitHub application is installed on your GitHub organization, but the
SonarCloud organization is not bound to it. Please read "How to bind an existing
The SonarCloud GitHub application is installed on your GitHub organization, but the
SonarCloud organization is not bound to it. Please read "How to bind an existing
organization?" section in the "Organizations" documentation page to fix your setup.

This means that your SonarCloud organization is not bound to GitHub or Bitbucket Cloud whereas you had already installed the SonarCloud application (probably to annotate pull requests). To fix your setup, here are the steps to follow.
@@ -49,3 +53,12 @@ This means that your SonarCloud organization is not bound to GitHub or Bitbucket
[[warning]]
| If you get a 405 error page from Bitbucket Cloud at this stage, this means that you did not approve a recent scope change - for which you should have received an email from Bitbucket Cloud. The easiest way to get around this is to uninstall the SonarCloud application in your Bitbucket Cloud "Install apps" settings, and reinstall it.
5. You are all set! You should see a Bitbucket Cloud icon close to the name of your organization at the top of the page.

### How to transfer ownership of an organization?

You may want to transfer ownership of you organization when you want to delete your account, or when you are leaving a team or company.
You can manage your organization members permissions in: "Administration > Permissions" and [grant "Administer Organization" permission](/organizations/manage-team/#granting-permissions) to another member.

### How to delete an organization?

You can delete your organization in: "Administration > Organization Settings > Delete Organization".

+ 33
- 4
server/sonar-docs/src/pages/user-guide/user-account.md 查看文件

@@ -5,16 +5,45 @@ url: /user-guide/user-account/

As a {instance} user you have your own space where you can see the things that are relevant to you:

## Home Page
## Profile

<!-- sonarqube -->

It gives you a summary of:

* your Groups
* your SCM accounts
- your Groups
- your SCM accounts

## Security

If <!-- sonarqube -->your instance is<!-- /sonarqube --> <!-- sonarcloud -->you are<!-- /sonarcloud --> not using a 3rd party authentication mechanism such as <!-- sonarqube -->LDAP or<!-- /sonarqube --> an OAuth provider (GitHub, Google Account, ...), you can change your password from here. Additionally, you can also manage your own authentication tokens.
If your instance is not using a 3rd party authentication mechanism such as LDAP or an OAuth provider (GitHub, Google Account, ...), you can change your password from here. Additionally, you can also manage your own authentication tokens.

You can create as many Tokens as you want. Once a Token is created, you can use it to perform analysis on a project where you have the [Execute Analysis](/instance-administration/security/) permission.

<!-- /sonarqube -->

<!-- sonarcloud -->

It gives you a summary of your SCM accounts and allows you to delete your account.

## Security

You can create as many Tokens as you want. Once a Token is created, you can use it to perform analysis on a project where you have the [Execute Analysis](/instance-administration/security/) permission.

## Organizations

This is an overview of all the organizations you are member of.

## Delete your user account

Go to [User > My Account > Profile](/#sonarcloud#/account) and click on **Delete**. Once your account is deleted, all you data will be removed except your login that will still be displayed in different places:

- issues assignee
- issues comments
- issues changelog

Note that you can manually unassign yourself from all your issues and/or remove your comments before deleting your account.

The information used to identify yourself in SCM (name, email) are part of the SCM data and can not be removed.

<!-- /sonarcloud -->

+ 1
- 0
server/sonar-docs/static/SonarCloudNavigationTree.json 查看文件

@@ -71,6 +71,7 @@
"/user-guide/visualizations/",
"/user-guide/sonarlint-notifications/",
"/user-guide/security-reports/",
"/user-guide/user-account/",
"/user-guide/user-token/",
"/user-guide/keyboard-shortcuts/"
]

+ 0
- 5
server/sonar-server/src/main/java/org/sonar/server/authentication/SafeModeUserSession.java 查看文件

@@ -91,11 +91,6 @@ public class SafeModeUserSession extends AbstractUserSession {
return Optional.empty();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return Optional.empty();
}

@Override
public boolean isLoggedIn() {
return false;

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/issue/ws/ChangelogAction.java 查看文件

@@ -127,7 +127,8 @@ public class ChangelogAction implements IssuesWsAction {
UserDto user = userUUuid == null ? null : results.users.get(userUUuid);
if (user != null) {
changelogBuilder.setUser(user.getLogin());
changelogBuilder.setUserName(user.getName());
changelogBuilder.setIsUserActive(user.isActive());
ofNullable(user.getName()).ifPresent(changelogBuilder::setUserName);
ofNullable(emptyToNull(user.getEmail())).ifPresent(email -> changelogBuilder.setAvatar(avatarFactory.create(user)));
}
change.diffs().entrySet().stream()

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/organization/OrganizationUpdater.java 查看文件

@@ -37,7 +37,7 @@ public interface OrganizationUpdater {
String PERM_TEMPLATE_DESCRIPTION_PATTERN = "Default permission template of organization %s";

/**
* Create a new non guarded organization with the specified properties and of which the specified user will assign
* Create a new organization with the specified properties and of which the specified user will assign
* Administer Organization permission.
* <p>
* This method does several operations at once:

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/organization/ws/AddMemberAction.java 查看文件

@@ -111,9 +111,9 @@ public class AddMemberAction implements OrganizationsWsAction {
AddMemberWsResponse.Builder response = AddMemberWsResponse.newBuilder();
User.Builder wsUser = User.newBuilder()
.setLogin(user.getLogin())
.setName(user.getName())
.setGroupCount(groups);
ofNullable(emptyToNull(user.getEmail())).ifPresent(text -> wsUser.setAvatar(avatarResolver.create(user)));
ofNullable(user.getName()).ifPresent(wsUser::setName);
response.setUser(wsUser);
return response.build();
}

+ 0
- 77
server/sonar-server/src/main/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsAction.java 查看文件

@@ -1,77 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.organization.ws;

import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.organization.OrganizationQuery;
import org.sonar.server.user.AbstractUserSession;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.UserSession;

public class DeleteEmptyPersonalOrgsAction implements OrganizationsWsAction {

private static final Logger LOGGER = Loggers.get(DeleteEmptyPersonalOrgsAction.class);

private static final String ACTION = "delete_empty_personal_orgs";

private final SystemPasscode passcode;
private final UserSession userSession;
private final OrganizationDeleter organizationDeleter;

public DeleteEmptyPersonalOrgsAction(SystemPasscode passcode, UserSession userSession, OrganizationDeleter organizationDeleter) {
this.passcode = passcode;
this.userSession = userSession;
this.organizationDeleter = organizationDeleter;
}

@Override
public void define(WebService.NewController context) {
context.createAction(ACTION)
.setDescription("Internal use. Requires system administration permission. Delete empty personal organizations.")
.setInternal(true)
.setPost(true)
.setHandler(this);
}

@Override
public void handle(Request request, Response response) throws Exception {
if (!passcode.isValid(request) && !userSession.isSystemAdministrator()) {
throw AbstractUserSession.insufficientPrivilegesException();
}

LOGGER.info("deleting empty personal organizations");

OrganizationQuery query = OrganizationQuery.newOrganizationQueryBuilder()
.setOnlyPersonal()
.setWithoutProjects()
.build();

organizationDeleter.deleteByQuery(query);

LOGGER.info("Deleted empty personal organizations");

response.noContent();
}

}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/organization/ws/OrganizationsWsModule.java 查看文件

@@ -52,9 +52,9 @@ public class OrganizationsWsModule extends Module {
CreateAction.class,
OrganizationDeleter.class,
DeleteAction.class,
DeleteEmptyPersonalOrgsAction.class,
RemoveMemberAction.class,
UpdateAction.class);
UpdateAction.class,
PreventUserDeletionAction.class);
}
}


+ 87
- 0
server/sonar-server/src/main/java/org/sonar/server/organization/ws/PreventUserDeletionAction.java 查看文件

@@ -0,0 +1,87 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.organization.ws;

import java.util.List;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.organization.OrganizationHelper;
import org.sonar.server.user.UserSession;
import org.sonarqube.ws.Organizations;

import static org.sonar.server.ws.WsUtils.writeProtobuf;

public class PreventUserDeletionAction implements OrganizationsWsAction {

private static final String ACTION = "prevent_user_deletion";

private final DbClient dbClient;
private final UserSession userSession;

public PreventUserDeletionAction(DbClient dbClient, UserSession userSession) {
this.dbClient = dbClient;
this.userSession = userSession;
}

@Override
public void define(WebService.NewController context) {
context.createAction(ACTION)
.setPost(false)
.setDescription("List organizations that prevent the deletion of the authenticated user.")
.setResponseExample(getClass().getResource("prevent_user_deletion-example.json"))
.setInternal(true)
.setSince("7.9")
.setHandler(this);
}

@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn();

try (DbSession dbSession = dbClient.openSession(false)) {
int userId = userSession.getUserId();
List<OrganizationDto> organizationsThatPreventUserDeletion = new OrganizationHelper(dbClient).selectOrganizationsWithLastAdmin(dbSession, userId);

Organizations.PreventUserDeletionWsResponse wsResponse = buildResponse(organizationsThatPreventUserDeletion);
writeProtobuf(wsResponse, request, response);
}
}

private Organizations.PreventUserDeletionWsResponse buildResponse(List<OrganizationDto> organizations) {
Organizations.PreventUserDeletionWsResponse.Builder response = Organizations.PreventUserDeletionWsResponse.newBuilder();
Organizations.PreventUserDeletionWsResponse.Organization.Builder wsOrganization = Organizations.PreventUserDeletionWsResponse.Organization.newBuilder();
organizations.forEach(o -> {
wsOrganization.clear();
response.addOrganizations(toOrganization(wsOrganization, o));
});
return response.build();
}

private static Organizations.PreventUserDeletionWsResponse.Organization.Builder toOrganization(
Organizations.PreventUserDeletionWsResponse.Organization.Builder builder, OrganizationDto organization) {
return builder
.setName(organization.getName())
.setKey(organization.getKey());
}
}

+ 2
- 2
server/sonar-server/src/main/java/org/sonar/server/organization/ws/SearchMembersAction.java 查看文件

@@ -137,9 +137,9 @@ public class SearchMembersAction implements OrganizationsWsAction {
String login = userDto.getLogin();
wsUser
.clear()
.setLogin(login)
.setName(userDto.getName());
.setLogin(login);
ofNullable(emptyToNull(userDto.getEmail())).ifPresent(text -> wsUser.setAvatar(avatarResolver.create(userDto)));
ofNullable(userDto.getName()).ifPresent(wsUser::setName);
ofNullable(groupCountByLogin).ifPresent(count -> wsUser.setGroupCount(groupCountByLogin.count(login)));
return wsUser;
})

+ 1
- 1
server/sonar-server/src/main/java/org/sonar/server/qualityprofile/ws/SearchUsersAction.java 查看文件

@@ -156,8 +156,8 @@ public class SearchUsersAction implements QProfileWsAction {
private SearchUsersResponse.User toUser(UserDto user, boolean isSelected) {
SearchUsersResponse.User.Builder builder = SearchUsersResponse.User.newBuilder()
.setLogin(user.getLogin())
.setName(user.getName())
.setSelected(isSelected);
ofNullable(user.getName()).ifPresent(builder::setName);
ofNullable(emptyToNull(user.getEmail())).ifPresent(e -> builder.setAvatar(avatarResolver.create(user)));
return builder
.build();

+ 0
- 5
server/sonar-server/src/main/java/org/sonar/server/user/DoPrivileged.java 查看文件

@@ -112,11 +112,6 @@ public final class DoPrivileged {
return Optional.empty();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return Optional.empty();
}

@Override
protected boolean hasPermissionImpl(OrganizationPermission permission, String organizationUuid) {
return true;

+ 0
- 5
server/sonar-server/src/main/java/org/sonar/server/user/ServerUserSession.java 查看文件

@@ -132,11 +132,6 @@ public class ServerUserSession extends AbstractUserSession {
return ofNullable(userDto).map(d -> computeIdentity(d).getExternalIdentity());
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return ofNullable(userDto).map(UserDto::getOrganizationUuid);
}

@Override
protected boolean hasPermissionImpl(OrganizationPermission permission, String organizationUuid) {
if (permissionsByOrganizationUuid == null) {

+ 0
- 5
server/sonar-server/src/main/java/org/sonar/server/user/ThreadLocalUserSession.java 查看文件

@@ -95,11 +95,6 @@ public class ThreadLocalUserSession implements UserSession {
return get().getExternalIdentity();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return get().getPersonalOrganizationUuid();
}

@Override
public boolean isLoggedIn() {
return get().isLoggedIn();

+ 0
- 5
server/sonar-server/src/main/java/org/sonar/server/user/UserSession.java 查看文件

@@ -148,11 +148,6 @@ public interface UserSession {
*/
Optional<ExternalIdentity> getExternalIdentity();

/**
* The UUID of the personal organization of the authenticated user.
*/
Optional<String> getPersonalOrganizationUuid();

/**
* Whether the user is logged-in or anonymous.
*/

+ 6
- 1
server/sonar-server/src/main/java/org/sonar/server/user/UserUpdater.java 查看文件

@@ -397,7 +397,7 @@ public class UserUpdater {
if (existingUser != null && matchingUser.getId().equals(existingUser.getId())) {
continue;
}
matchingUsersWithoutExistingUser.add(matchingUser.getName() + " (" + matchingUser.getLogin() + ")");
matchingUsersWithoutExistingUser.add(getNameOrLogin(matchingUser) + " (" + matchingUser.getLogin() + ")");
}
if (!matchingUsersWithoutExistingUser.isEmpty()) {
messages.add(format("The scm account '%s' is already used by user(s) : '%s'", scmAccount, Joiner.on(", ").join(matchingUsersWithoutExistingUser)));
@@ -408,6 +408,11 @@ public class UserUpdater {
return isValid;
}

private static String getNameOrLogin(UserDto user) {
String name = user.getName();
return name != null ? name : user.getLogin();
}

private static List<String> sanitizeScmAccounts(@Nullable List<String> scmAccounts) {
if (scmAccounts != null) {
return new HashSet<>(scmAccounts).stream()

+ 1
- 2
server/sonar-server/src/main/java/org/sonar/server/user/ws/CreateAction.java 查看文件

@@ -150,8 +150,7 @@ public class CreateAction implements UsersWsAction {
}));
}
checkArgument(!existingUser.isActive(), "An active user with login '%s' already exists", login);
return buildResponse(userUpdater.reactivateAndCommit(dbSession, existingUser, newUser.build(), u -> {
}));
return buildResponse(userUpdater.reactivateAndCommit(dbSession, existingUser, newUser.build(), u -> {}));
}
}


+ 35
- 29
server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java 查看文件

@@ -19,23 +19,26 @@
*/
package org.sonar.server.user.ws;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.sonar.api.config.Configuration;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.NewAction;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.permission.OrganizationPermission;
import org.sonar.db.organization.OrganizationHelper;
import org.sonar.db.property.PropertyQuery;
import org.sonar.db.user.UserDto;
import org.sonar.server.exceptions.BadRequestException;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.organization.DefaultOrganizationProvider;
import org.sonar.server.user.UserSession;
import org.sonar.server.user.index.UserIndexer;
@@ -43,11 +46,14 @@ import org.sonar.server.user.index.UserIndexer;
import static java.lang.String.format;
import static java.util.Collections.singletonList;
import static org.sonar.api.CoreProperties.DEFAULT_ISSUE_ASSIGNEE;
import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
import static org.sonar.server.ws.WsUtils.checkFound;
import static org.sonar.server.ws.WsUtils.checkRequest;

public class DeactivateAction implements UsersWsAction {

private static final Logger LOGGER = Loggers.get(DeactivateAction.class);

private static final String PARAM_LOGIN = "login";

private final DbClient dbClient;
@@ -55,14 +61,16 @@ public class DeactivateAction implements UsersWsAction {
private final UserSession userSession;
private final UserJsonWriter userWriter;
private final DefaultOrganizationProvider defaultOrganizationProvider;
private final boolean isSonarCloud;

public DeactivateAction(DbClient dbClient, UserIndexer userIndexer, UserSession userSession, UserJsonWriter userWriter,
DefaultOrganizationProvider defaultOrganizationProvider) {
DefaultOrganizationProvider defaultOrganizationProvider, Configuration configuration) {
this.dbClient = dbClient;
this.userIndexer = userIndexer;
this.userSession = userSession;
this.userWriter = userWriter;
this.defaultOrganizationProvider = defaultOrganizationProvider;
this.isSonarCloud = configuration.getBoolean(SONARCLOUD_ENABLED.getKey()).orElse(false);
}

@Override
@@ -82,10 +90,18 @@ public class DeactivateAction implements UsersWsAction {

@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn().checkIsSystemAdministrator();
String login;

String login = request.mandatoryParam(PARAM_LOGIN);
checkRequest(!login.equals(userSession.getLogin()), "Self-deactivation is not possible");
if (isSonarCloud) {
login = request.mandatoryParam(PARAM_LOGIN);
if (!login.equals(userSession.getLogin()) && !userSession.checkLoggedIn().isSystemAdministrator()) {
throw new ForbiddenException("Insufficient privileges");
}
} else {
userSession.checkLoggedIn().checkIsSystemAdministrator();
login = request.mandatoryParam(PARAM_LOGIN);
checkRequest(!login.equals(userSession.getLogin()), "Self-deactivation is not possible");
}

try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
@@ -103,13 +119,23 @@ public class DeactivateAction implements UsersWsAction {
dbClient.qProfileEditUsersDao().deleteByUser(dbSession, user);
dbClient.organizationMemberDao().deleteByUserId(dbSession, userId);
dbClient.userPropertiesDao().deleteByUser(dbSession, user);
dbClient.userDao().deactivateUser(dbSession, user);
deactivateUser(dbSession, user);
userIndexer.commitAndIndex(dbSession, user);

LOGGER.info("Deactivate user: {}; by admin: {}", login, userSession.isSystemAdministrator());
}

writeResponse(response, login);
}

private void deactivateUser(DbSession dbSession, UserDto user) {
if (isSonarCloud) {
dbClient.userDao().deactivateSonarCloudUser(dbSession, user);
} else {
dbClient.userDao().deactivateUser(dbSession, user);
}
}

private void writeResponse(Response response, String login) {
try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = dbClient.userDao().selectByLogin(dbSession, login);
@@ -129,38 +155,18 @@ public class DeactivateAction implements UsersWsAction {
}

private void ensureNotLastAdministrator(DbSession dbSession, UserDto user) {
List<String> problematicOrgs = selectOrganizationsWithNoMoreAdministrators(dbSession, user);
List<OrganizationDto> problematicOrgs = new OrganizationHelper(dbClient).selectOrganizationsWithLastAdmin(dbSession, user.getId());
if (problematicOrgs.isEmpty()) {
return;
}
checkRequest(problematicOrgs.size() != 1 || !defaultOrganizationProvider.get().getUuid().equals(problematicOrgs.get(0)),
checkRequest(problematicOrgs.size() != 1 || !defaultOrganizationProvider.get().getUuid().equals(problematicOrgs.get(0).getUuid()),
"User is last administrator, and cannot be deactivated");
String keys = problematicOrgs
.stream()
.map(orgUuid -> selectOrganizationByUuid(dbSession, orgUuid, user))
.map(OrganizationDto::getKey)
.sorted()
.collect(Collectors.joining(", "));
throw BadRequestException.create(format("User '%s' is last administrator of organizations [%s], and cannot be deactivated", user.getLogin(), keys));
}

private List<String> selectOrganizationsWithNoMoreAdministrators(DbSession dbSession, UserDto user) {
Set<String> organizationUuids = dbClient.authorizationDao().selectOrganizationUuidsOfUserWithGlobalPermission(
dbSession, user.getId(), OrganizationPermission.ADMINISTER.getKey());
List<String> problematicOrganizations = new ArrayList<>();
for (String organizationUuid : organizationUuids) {
int remaining = dbClient.authorizationDao().countUsersWithGlobalPermissionExcludingUser(dbSession,
organizationUuid, OrganizationPermission.ADMINISTER.getKey(), user.getId());
if (remaining == 0) {
problematicOrganizations.add(organizationUuid);
}
}
return problematicOrganizations;
}

private OrganizationDto selectOrganizationByUuid(DbSession dbSession, String orgUuid, UserDto user) {
return dbClient.organizationDao()
.selectByUuid(dbSession, orgUuid)
.orElseThrow(() -> new IllegalStateException("Organization with UUID " + orgUuid + " does not exist in DB but is referenced in permissions of user " + user.getLogin()));
}
}

+ 1
- 14
server/sonar-server/src/main/java/org/sonar/server/user/ws/UpdateLoginAction.java 查看文件

@@ -81,11 +81,7 @@ public class UpdateLoginAction implements UsersWsAction {
String newLogin = request.mandatoryParam(PARAM_NEW_LOGIN);
try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = getUser(dbSession, login);
userUpdater.updateAndCommit(
dbSession,
user,
new UpdateUser().setLogin(newLogin),
u -> updatePersonalOrganization(dbSession, u));
userUpdater.updateAndCommit(dbSession, user, new UpdateUser().setLogin(newLogin), u -> {});
response.noContent();
}
}
@@ -98,13 +94,4 @@ public class UpdateLoginAction implements UsersWsAction {
return user;
}

private void updatePersonalOrganization(DbSession dbSession, UserDto user) {
String personalOrganizationUuid = user.getOrganizationUuid();
if (personalOrganizationUuid == null) {
return;
}
dbClient.organizationDao().selectByUuid(dbSession, personalOrganizationUuid)
.ifPresent(organization -> organizationUpdater.updateOrganizationKey(dbSession, organization, user.getLogin()));
}

}

+ 2
- 1
server/sonar-server/src/main/java/org/sonar/server/user/ws/UserJsonWriter.java 查看文件

@@ -28,6 +28,7 @@ import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.user.UserDto;
import org.sonar.server.user.UserSession;

import static java.util.Optional.ofNullable;
import static org.sonar.server.ws.JsonWriterUtils.isFieldNeeded;
import static org.sonar.server.ws.JsonWriterUtils.writeIfNeeded;

@@ -61,7 +62,7 @@ public class UserJsonWriter {
public void write(JsonWriter json, UserDto user, Collection<String> groups, @Nullable Collection<String> fields) {
json.beginObject();
json.prop(FIELD_LOGIN, user.getLogin());
writeIfNeeded(json, user.getName(), FIELD_NAME, fields);
ofNullable(user.getName()).ifPresent(name -> writeIfNeeded(json, name, FIELD_NAME, fields));
if (userSession.isLoggedIn()) {
writeIfNeeded(json, user.getEmail(), FIELD_EMAIL, fields);
writeIfNeeded(json, user.isActive(), FIELD_ACTIVE, fields);

+ 1
- 0
server/sonar-server/src/main/resources/org/sonar/server/issue/ws/changelog-example.json 查看文件

@@ -3,6 +3,7 @@
{
"user": "john.smith",
"userName": "John Smith",
"isUserActive": true,
"avatar": "b0d8c6e5ea589e6fc3d3e08afb1873bb",
"creationDate": "2014-03-04T23:03:44+0100",
"diffs": [

+ 12
- 0
server/sonar-server/src/main/resources/org/sonar/server/organization/ws/prevent_user_deletion-example.json 查看文件

@@ -0,0 +1,12 @@
{
"organizations": [
{
"key": "foo-company",
"name": "Foo Company"
},
{
"key": "bar-company",
"name": "Bar Company"
}
]
}

+ 20
- 0
server/sonar-server/src/test/java/org/sonar/server/issue/ws/ChangelogActionTest.java 查看文件

@@ -90,6 +90,7 @@ public class ChangelogActionTest {
assertThat(result.getChangelogList()).hasSize(1);
assertThat(result.getChangelogList().get(0).getUser()).isNotNull().isEqualTo(user.getLogin());
assertThat(result.getChangelogList().get(0).getUserName()).isNotNull().isEqualTo(user.getName());
assertThat(result.getChangelogList().get(0).getIsUserActive()).isTrue();
assertThat(result.getChangelogList().get(0).getAvatar()).isNotNull().isEqualTo("93942e96f5acd83e2e047ad8fe03114d");
assertThat(result.getChangelogList().get(0).getCreationDate()).isNotEmpty();
assertThat(result.getChangelogList().get(0).getDiffsList()).extracting(Diff::getKey, Diff::getOldValue, Diff::getNewValue).containsOnly(tuple("severity", "MAJOR", "BLOCKER"));
@@ -196,6 +197,25 @@ public class ChangelogActionTest {
assertThat(result.getChangelogList().get(0).getDiffsList()).isNotEmpty();
}

@Test
public void return_changelog_on_deactivated_user() {
UserDto user = db.users().insertDisabledUser();
IssueDto issueDto = db.issues().insertIssue(newIssue());
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());

assertThat(result.getChangelogList()).hasSize(1);
assertThat(result.getChangelogList().get(0).getUser()).isEqualTo(user.getLogin());
assertThat(result.getChangelogList().get(0).getIsUserActive()).isFalse();
assertThat(result.getChangelogList().get(0).getUserName()).isEqualTo(user.getName());
assertThat(result.getChangelogList().get(0).hasAvatar()).isFalse();
assertThat(result.getChangelogList().get(0).getDiffsList()).isNotEmpty();
}

@Test
public void return_multiple_diffs() {
UserDto user = insertUser();

+ 1
- 6
server/sonar-server/src/test/java/org/sonar/server/organization/OrganizationUpdaterImplTest.java 查看文件

@@ -73,9 +73,6 @@ import static org.sonar.server.organization.OrganizationUpdater.NewOrganization.

public class OrganizationUpdaterImplTest {
private static final long A_DATE = 12893434L;
private static final String A_LOGIN = "a-login";
private static final String SLUG_OF_A_LOGIN = "slug-of-a-login";
private static final String A_NAME = "a name";

private OrganizationUpdater.NewOrganization FULL_POPULATED_NEW_ORGANIZATION = newOrganizationBuilder()
.setName("a-name")
@@ -116,7 +113,7 @@ public class OrganizationUpdaterImplTest {
builtInQProfileRepositoryRule, defaultGroupCreator, permissionService);

@Test
public void create_creates_unguarded_organization_with_properties_from_NewOrganization_arg() throws OrganizationUpdater.KeyConflictException {
public void create_creates_organization_with_properties_from_NewOrganization_arg() throws OrganizationUpdater.KeyConflictException {
builtInQProfileRepositoryRule.initialize();
UserDto user = db.users().insertUser();
db.qualityGates().insertBuiltInQualityGate();
@@ -130,7 +127,6 @@ public class OrganizationUpdaterImplTest {
assertThat(organization.getDescription()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getDescription());
assertThat(organization.getUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getUrl());
assertThat(organization.getAvatarUrl()).isEqualTo(FULL_POPULATED_NEW_ORGANIZATION.getAvatar());
assertThat(organization.isGuarded()).isFalse();
assertThat(organization.getSubscription()).isEqualTo(Subscription.FREE);
assertThat(organization.getCreatedAt()).isEqualTo(A_DATE);
assertThat(organization.getUpdatedAt()).isEqualTo(A_DATE);
@@ -175,7 +171,6 @@ public class OrganizationUpdaterImplTest {
assertThat(organization.getDescription()).isNull();
assertThat(organization.getUrl()).isNull();
assertThat(organization.getAvatarUrl()).isNull();
assertThat(organization.isGuarded()).isFalse();
}

@Test

+ 0
- 141
server/sonar-server/src/test/java/org/sonar/server/organization/ws/DeleteEmptyPersonalOrgsActionTest.java 查看文件

@@ -1,141 +0,0 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.organization.ws;

import java.util.Arrays;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ResourceTypesRule;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.es.EsClient;
import org.sonar.server.es.EsTester;
import org.sonar.server.es.ProjectIndexersImpl;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.organization.BillingValidationsProxyImpl;
import org.sonar.server.project.ProjectLifeCycleListener;
import org.sonar.server.project.ProjectLifeCycleListenersImpl;
import org.sonar.server.qualityprofile.QProfileFactoryImpl;
import org.sonar.server.qualityprofile.index.ActiveRuleIndexer;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.user.SystemPasscode;
import org.sonar.server.user.index.UserIndexer;
import org.sonar.server.ws.WsActionTester;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;

public class DeleteEmptyPersonalOrgsActionTest {

@Rule
public final UserSessionRule userSession = UserSessionRule.standalone();

@Rule
public final DbTester db = DbTester.create(new System2());
private final DbClient dbClient = db.getDbClient();

@Rule
public final EsTester es = EsTester.create();
private final EsClient esClient = es.client();

@Rule
public final ExpectedException expectedException = ExpectedException.none();

private SystemPasscode passcode = mock(SystemPasscode.class);
private final OrganizationDeleter organizationDeleter = new OrganizationDeleter(dbClient,
new ComponentCleanerService(dbClient, new ResourceTypesRule(), new ProjectIndexersImpl()),
new UserIndexer(dbClient, esClient),
new QProfileFactoryImpl(dbClient, UuidFactoryFast.getInstance(), new System2(), new ActiveRuleIndexer(dbClient, esClient)),
new ProjectLifeCycleListenersImpl(new ProjectLifeCycleListener[0]),
new BillingValidationsProxyImpl());

private final DeleteEmptyPersonalOrgsAction underTest = new DeleteEmptyPersonalOrgsAction(passcode, userSession, organizationDeleter);
private final WsActionTester ws = new WsActionTester(underTest);

@Test
public void request_fails_if_user_is_not_root() {
userSession.logIn();

expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privileges");

ws.newRequest().execute();
}

@Test
public void delete_empty_personal_orgs() {
UserDto admin = db.users().insertUser();
db.users().insertPermissionOnUser(admin, ADMINISTER);
userSession.logIn().setSystemAdministrator();

doRun();
}

@Test
public void authenticate_with_system_passcode() {
when(passcode.isValid(any())).thenReturn(true);

doRun();
}

private void doRun() {
OrganizationDto emptyPersonal = db.organizations().insert(o -> o.setGuarded(true));
db.users().insertUser(u -> u.setOrganizationUuid(emptyPersonal.getUuid()));

OrganizationDto nonEmptyPersonal = db.organizations().insert(o -> o.setGuarded(true));
db.users().insertUser(u -> u.setOrganizationUuid(nonEmptyPersonal.getUuid()));
db.components().insertPublicProject(nonEmptyPersonal);

OrganizationDto emptyRegular = db.organizations().insert();

OrganizationDto nonEmptyRegular = db.organizations().insert();
db.components().insertPublicProject(nonEmptyRegular);

ws.newRequest().execute();

List<String> notDeleted = Arrays.asList(
db.getDefaultOrganization().getUuid(),
nonEmptyPersonal.getUuid(),
emptyRegular.getUuid(),
nonEmptyRegular.getUuid());

assertThat(dbClient.organizationDao().selectAllUuids(db.getSession()))
.containsExactlyInAnyOrderElementsOf(notDeleted);
}

@Test
public void definition() {
WebService.Action action = ws.getDef();
assertThat(action.isPost()).isTrue();
assertThat(action.isInternal()).isTrue();
assertThat(action.handler()).isNotNull();
}
}

+ 208
- 0
server/sonar-server/src/test/java/org/sonar/server/organization/ws/PreventUserDeletionActionTest.java 查看文件

@@ -0,0 +1,208 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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.organization.ws;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import java.util.List;
import java.util.Random;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.exceptions.UnauthorizedException;
import org.sonar.server.tester.UserSessionRule;
import org.sonar.server.ws.WsActionTester;
import org.sonarqube.ws.Organizations;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
import static org.sonar.test.JsonAssert.assertJson;

@RunWith(DataProviderRunner.class)
public class PreventUserDeletionActionTest {

private static final Random RANDOM = new Random();

@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

@Rule
public DbTester db = DbTester.create(mock(System2.class)).setDisableDefaultOrganization(true);

@Rule
public ExpectedException expectedException = ExpectedException.none();

private final PreventUserDeletionAction underTest = new PreventUserDeletionAction(db.getDbClient(), userSession);
private final WsActionTester ws = new WsActionTester(underTest);

@Test
public void fail_if_user_is_not_logged_in() {
UserDto user1 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert();
GroupDto group1 = db.users().insertGroup(org1);
db.users().insertMember(group1, user1);

expectedException.expect(UnauthorizedException.class);

call();
}

@Test
public void returns_empty_list_when_user_is_not_admin_of_any_orgs() {
UserDto user1 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert();
GroupDto group1 = db.users().insertGroup(org1);
db.users().insertMember(group1, user1);

userSession.logIn(user1);
assertThat(call().getOrganizationsList()).isEmpty();
}

@Test
public void returns_orgs_where_user_is_last_admin() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert();
OrganizationDto org2 = db.organizations().insert();

setAsDirectOrIndirectAdmin(user1, org1);
setAsDirectOrIndirectAdmin(user2, org1);
setAsDirectOrIndirectAdmin(user1, org2);

userSession.logIn(user1);
assertThat(call().getOrganizationsList())
.extracting(Organizations.Organization::getKey)
.containsExactly(org2.getKey());
}

@Test
@UseDataProvider("adminUserCombinationsAndExpectedOrgKeys")
public void returns_correct_orgs_for_interesting_combinations_of_last_admin_or_not(
boolean user2IsAdminOfOrg1, boolean user1IsAdminOfOrg2, boolean user2IsAdminOfOrg2, List<String> expectedOrgKeys) {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert(o -> o.setKey("org1"));
OrganizationDto org2 = db.organizations().insert(o -> o.setKey("org2"));

setAsDirectOrIndirectAdmin(user1, org1);
if (user2IsAdminOfOrg1) {
setAsDirectOrIndirectAdmin(user2, org1);
}
if (user1IsAdminOfOrg2) {
setAsDirectOrIndirectAdmin(user1, org2);
}
if (user2IsAdminOfOrg2) {
setAsDirectOrIndirectAdmin(user2, org2);
}

userSession.logIn(user1);
assertThat(call().getOrganizationsList())
.extracting(Organizations.Organization::getKey)
.containsExactlyInAnyOrderElementsOf(expectedOrgKeys);
}

@DataProvider
public static Object[][] adminUserCombinationsAndExpectedOrgKeys() {
return new Object[][] {
// note: user1 is always admin of org1
// param 1: user2 is admin of org1
// param 2: user1 is admin of org2
// param 3: user2 is admin of org2
// param 4: list of orgs preventing user1 to delete
{true, true, true, emptyList()},
{true, true, false, singletonList("org2")},
{true, false, true, emptyList()},
{true, false, false, emptyList()},
{false, true, true, singletonList("org1")},
{false, true, false, asList("org1", "org2")},
{false, false, true, singletonList("org1")},
{false, false, false, singletonList("org1")},
};
}

@Test
public void json_example() {
UserDto user1 = db.users().insertUser();

OrganizationDto org1 = db.organizations().insert(o -> {
o.setKey("foo-company");
o.setName("Foo Company");
});
OrganizationDto org2 = db.organizations().insert(o -> {
o.setKey("bar-company");
o.setName("Bar Company");
});

setAsDirectOrIndirectAdmin(user1, org1);
setAsDirectOrIndirectAdmin(user1, org2);

userSession.logIn(user1);

String result = ws.newRequest()
.execute()
.getInput();

assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString());
}

@Test
public void definition() {
WebService.Action action = ws.getDef();

assertThat(action.key()).isEqualTo("prevent_user_deletion");
assertThat(action.params()).isEmpty();
assertThat(action.description()).isNotEmpty();
assertThat(action.responseExampleAsString()).isNotEmpty();
assertThat(action.since()).isEqualTo("7.9");
assertThat(action.isInternal()).isTrue();
assertThat(action.isPost()).isFalse();
}

private void setAsDirectOrIndirectAdmin(UserDto user, OrganizationDto organization) {
boolean useDirectAdmin = RANDOM.nextBoolean();
if (useDirectAdmin) {
db.users().insertPermissionOnUser(organization, user, ADMINISTER);
} else {
GroupDto group = db.users().insertGroup(organization);
db.users().insertPermissionOnGroup(group, ADMINISTER);
db.users().insertMember(group, user);
}
}

private Organizations.SearchWsResponse call() {
return ws.newRequest().executeProtobuf(Organizations.SearchWsResponse.class);
}
}

+ 2
- 20
server/sonar-server/src/test/java/org/sonar/server/organization/ws/SearchActionTest.java 查看文件

@@ -99,22 +99,6 @@ public class SearchActionTest {
tuple(groupAdminOrganization.getKey(), true, true));
}

@Test
public void root_can_do_everything() {
OrganizationDto organization = db.organizations().insert();
OrganizationDto guardedOrganization = db.organizations().insert(dto -> dto.setGuarded(true));
UserDto user = db.users().insertUser();
userSession.logIn(user).setRoot();

SearchWsResponse result = call(ws.newRequest());

assertThat(result.getOrganizationsList())
.extracting(Organization::getKey, o -> o.getActions().getAdmin(), o -> o.getActions().getDelete(), o -> o.getActions().getProvision())
.containsExactlyInAnyOrder(
tuple(organization.getKey(), true, true, true),
tuple(guardedOrganization.getKey(), true, true, true));
}

@Test
public void provision_action_available_for_each_organization() {
OrganizationDto userProvisionOrganization = db.organizations().insert();
@@ -402,8 +386,7 @@ public class SearchActionTest {
.setDescription("The Bar company produces quality software too.")
.setUrl("https://www.bar.com")
.setAvatarUrl("https://www.bar.com/logo.png")
.setSubscription(PAID)
.setGuarded(false));
.setSubscription(PAID));
OrganizationDto fooOrganization = db.organizations().insert(organization -> organization
.setUuid(Uuids.UUID_EXAMPLE_01)
.setKey("foo-company")
@@ -411,8 +394,7 @@ public class SearchActionTest {
.setSubscription(FREE)
.setDescription(null)
.setUrl(null)
.setAvatarUrl(null)
.setGuarded(true));
.setAvatarUrl(null));
UserDto user = db.users().insertUser();
db.organizations().addMember(barOrganization, user);
db.organizations().addMember(fooOrganization, user);

+ 0
- 5
server/sonar-server/src/test/java/org/sonar/server/tester/AnonymousMockUserSession.java 查看文件

@@ -75,11 +75,6 @@ public class AnonymousMockUserSession extends AbstractMockUserSession<AnonymousM
return Optional.empty();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return Optional.empty();
}

@Override
public boolean hasMembershipImpl(OrganizationDto organizationDto) {
return false;

+ 0
- 4
server/sonar-server/src/test/java/org/sonar/server/tester/MockUserSession.java 查看文件

@@ -144,8 +144,4 @@ public class MockUserSession extends AbstractMockUserSession<MockUserSession> {
return Optional.ofNullable(externalIdentity);
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return Optional.empty();
}
}

+ 0
- 5
server/sonar-server/src/test/java/org/sonar/server/tester/UserSessionRule.java 查看文件

@@ -289,11 +289,6 @@ public class UserSessionRule implements TestRule, UserSession {
return currentUserSession.getExternalIdentity();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
return currentUserSession.getPersonalOrganizationUuid();
}

@Override
public boolean isLoggedIn() {
return currentUserSession.isLoggedIn();

+ 2
- 2
server/sonar-server/src/test/java/org/sonar/server/ui/ws/OrganizationActionTest.java 查看文件

@@ -81,7 +81,7 @@ public class OrganizationActionTest {
initWithPages(
Page.builder("my-plugin/org-page").setName("Organization page").setScope(ORGANIZATION).build(),
Page.builder("my-plugin/org-admin-page").setName("Organization admin page").setScope(ORGANIZATION).setAdmin(true).build());
OrganizationDto organization = db.organizations().insert(dto -> dto.setGuarded(true));
OrganizationDto organization = db.organizations().insert();
userSession.logIn()
.addPermission(PROVISION_PROJECTS, organization);

@@ -214,7 +214,7 @@ public class OrganizationActionTest {
initWithPages(
Page.builder("my-plugin/org-page").setName("Organization page").setScope(ORGANIZATION).build(),
Page.builder("my-plugin/org-admin-page").setName("Organization admin page").setScope(ORGANIZATION).setAdmin(true).build());
OrganizationDto organization = db.organizations().insert(dto -> dto.setGuarded(true));
OrganizationDto organization = db.organizations().insert();
userSession.logIn()
.addPermission(ADMINISTER, organization)
.addPermission(PROVISION_PROJECTS, organization);

+ 0
- 3
server/sonar-server/src/test/java/org/sonar/server/user/ServerUserSessionTest.java 查看文件

@@ -24,14 +24,11 @@ import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.ResourceTypes;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ResourceTypesRule;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.UserDto;

+ 0
- 5
server/sonar-server/src/test/java/org/sonar/server/user/TestUserSessionFactory.java 查看文件

@@ -97,11 +97,6 @@ public class TestUserSessionFactory implements UserSessionFactory {
throw notImplemented();
}

@Override
public Optional<String> getPersonalOrganizationUuid() {
throw notImplemented();
}

@Override
public boolean isLoggedIn() {
return user != null;

+ 48
- 8
server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java 查看文件

@@ -24,6 +24,7 @@ import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.config.internal.MapSettings;
import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
import org.sonar.api.utils.System2;
import org.sonar.db.DbClient;
@@ -63,6 +64,7 @@ import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES;
import static org.sonar.db.permission.OrganizationPermission.SCAN;
import static org.sonar.db.property.PropertyTesting.newUserPropertyDto;
import static org.sonar.process.ProcessProperties.Property.SONARCLOUD_ENABLED;
import static org.sonar.server.user.index.UserIndexDefinition.FIELD_ACTIVE;
import static org.sonar.server.user.index.UserIndexDefinition.FIELD_UUID;
import static org.sonar.test.JsonAssert.assertJson;
@@ -87,9 +89,9 @@ public class DeactivateActionTest {
private DbClient dbClient = db.getDbClient();
private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
private DbSession dbSession = db.getSession();
private WsActionTester ws = new WsActionTester(new DeactivateAction(
dbClient, userIndexer, userSession, new UserJsonWriter(userSession), defaultOrganizationProvider));
private MapSettings settings = new MapSettings();
private WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession,
new UserJsonWriter(userSession), defaultOrganizationProvider, settings.asConfig()));

@Test
public void deactivate_user_and_delete_his_related_data() {
@@ -102,7 +104,7 @@ public class DeactivateActionTest {

deactivate(user.getLogin());

verifyThatUserIsDeactivated(user.getLogin());
verifyThatUserIsDeactivated(user.getLogin(), false);
assertThat(es.client().prepareSearch(UserIndexDefinition.TYPE_USER)
.setQuery(boolQuery()
.must(termQuery(FIELD_UUID, user.getUuid()))
@@ -242,7 +244,32 @@ public class DeactivateActionTest {
}

@Test
public void cannot_deactivate_self() {
public void user_can_deactivate_itself_on_sonarcloud() {
WsActionTester customWs = newSonarCloudWs();

UserDto user = db.users().insertUser();
userSession.logIn(user.getLogin());

deactivate(customWs, user.getLogin());

verifyThatUserIsDeactivated(user.getLogin(), true);
}

@Test
public void user_cannot_deactivate_another_user_on_sonarcloud() {
WsActionTester customWs = newSonarCloudWs();

UserDto user = db.users().insertUser();
userSession.logIn(user.getLogin());

expectedException.expect(ForbiddenException.class);
expectedException.expectMessage("Insufficient privilege");

deactivate(customWs, "other user");
}

@Test
public void user_cannot_deactivate_itself_on_sonarqube() {
UserDto user = db.users().insertUser();
userSession.logIn(user.getLogin()).setSystemAdministrator();

@@ -263,7 +290,7 @@ public class DeactivateActionTest {
}

@Test
public void deactivation_requires_administrator_permission() {
public void deactivation_requires_administrator_permission_on_sonarqube() {
userSession.logIn();

expectedException.expect(ForbiddenException.class);
@@ -346,7 +373,7 @@ public class DeactivateActionTest {

deactivate(admin.getLogin());

verifyThatUserIsDeactivated(admin.getLogin());
verifyThatUserIsDeactivated(admin.getLogin(), false);
verifyThatUserExists(anotherAdmin.getLogin());
}

@@ -377,6 +404,10 @@ public class DeactivateActionTest {
}

private TestResponse deactivate(@Nullable String login) {
return deactivate(ws, login);
}

private TestResponse deactivate(WsActionTester ws, @Nullable String login) {
TestRequest request = ws.newRequest()
.setMethod("POST");
Optional.ofNullable(login).ifPresent(t -> request.setParam("login", login));
@@ -387,11 +418,20 @@ public class DeactivateActionTest {
assertThat(db.users().selectUserByLogin(login)).isPresent();
}

private void verifyThatUserIsDeactivated(String login) {
private void verifyThatUserIsDeactivated(String login, boolean isSonarCloud) {
Optional<UserDto> user = db.users().selectUserByLogin(login);
assertThat(user).isPresent();
assertThat(user.get().isActive()).isFalse();
assertThat(user.get().getEmail()).isNull();
assertThat(user.get().getScmAccountsAsList()).isEmpty();
if (isSonarCloud) {
assertThat(user.get().getName()).isNull();
}
}

private WsActionTester newSonarCloudWs() {
settings.setProperty(SONARCLOUD_ENABLED.getKey(), true);
return new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession,
new UserJsonWriter(userSession), defaultOrganizationProvider, settings.asConfig()));
}
}

+ 0
- 18
server/sonar-server/src/test/java/org/sonar/server/user/ws/UpdateLoginActionTest.java 查看文件

@@ -119,24 +119,6 @@ public class UpdateLoginActionTest {
assertThat(userReloaded.getSalt()).isNull();
}

@Test
public void update_personal_organization_when_updating_login() {
userSession.logIn().setSystemAdministrator();
String oldLogin = "old_login";
OrganizationDto personalOrganization = db.organizations().insert(o -> o.setKey(oldLogin).setGuarded(true));
UserDto user = db.users().insertUser(u -> u.setLogin(oldLogin).setOrganizationUuid(personalOrganization.getUuid()));

ws.newRequest()
.setParam("login", oldLogin)
.setParam("newLogin", "new_login")
.execute();

UserDto userReloaded = db.getDbClient().userDao().selectByUuid(db.getSession(), user.getUuid());
assertThat(userReloaded.getLogin()).isEqualTo("new_login");
OrganizationDto organizationReloaded = db.getDbClient().organizationDao().selectByUuid(db.getSession(), personalOrganization.getUuid()).get();
assertThat(organizationReloaded.getKey()).isEqualTo("new_login");
}

@Test
public void fail_with_IAE_when_new_login_is_already_used() {
userSession.logIn().setSystemAdministrator();

+ 1
- 1
server/sonar-web/package.json 查看文件

@@ -41,7 +41,7 @@
"regenerator-runtime": "0.13.2",
"remark-custom-blocks": "2.3.0",
"remark-slug": "5.1.0",
"sonar-ui-common": "0.0.10",
"sonar-ui-common": "0.0.11",
"unist-util-visit": "1.4.0",
"valid-url": "1.0.9",
"whatwg-fetch": "2.0.4"

+ 5
- 9
server/sonar-web/src/main/js/api/issues.ts 查看文件

@@ -26,7 +26,7 @@ export interface IssueResponse {
components?: Array<{ key: string; name: string }>;
issue: RawIssue;
rules?: Array<{}>;
users?: Array<{ login: string }>;
users?: Array<T.UserBase>;
}

interface IssuesResponse {
@@ -37,13 +37,9 @@ interface IssuesResponse {
values: { count: number; val: string }[];
}>;
issues: RawIssue[];
paging: {
pageIndex: number;
pageSize: number;
total: number;
};
paging: T.Paging;
rules?: Array<{}>;
users?: { login: string }[];
users?: Array<T.UserBase>;
}

type FacetName =
@@ -109,8 +105,8 @@ export function searchIssueTags(data: {
.catch(throwGlobalError);
}

export function getIssueChangelog(issue: string): Promise<any> {
return getJSON('/api/issues/changelog', { issue }).then(r => r.changelog, throwGlobalError);
export function getIssueChangelog(issue: string): Promise<{ changelog: T.IssueChangelog[] }> {
return getJSON('/api/issues/changelog', { issue }).catch(throwGlobalError);
}

export function getIssueFilters() {

+ 6
- 0
server/sonar-web/src/main/js/api/organizations.ts 查看文件

@@ -52,6 +52,12 @@ export function getOrganizationNavigation(key: string): Promise<GetOrganizationN
);
}

export function getOrganizationsThatPreventDeletion(): Promise<{
organizations: T.Organization[];
}> {
return getJSON('/api/organizations/prevent_user_deletion').catch(throwGlobalError);
}

export function createOrganization(
data: T.OrganizationBase & { installationId?: string }
): Promise<T.Organization> {

+ 2
- 7
server/sonar-web/src/main/js/api/quality-profiles.ts 查看文件

@@ -194,13 +194,8 @@ export interface SearchUsersGroupsParameters {
selected?: 'all' | 'selected' | 'deselected';
}

export interface SearchUsersResponse {
users: Array<{
avatar?: string;
login: string;
name: string;
selected?: boolean;
}>;
interface SearchUsersResponse {
users: T.UserSelected[];
paging: T.Paging;
}


+ 1
- 7
server/sonar-web/src/main/js/api/user_groups.ts 查看文件

@@ -30,12 +30,6 @@ export function searchUsersGroups(data: {
return getJSON('/api/user_groups/search', data).catch(throwGlobalError);
}

export interface GroupUser {
login: string;
name: string;
selected: boolean;
}

export function getUsersInGroup(data: {
id?: number;
name?: string;
@@ -44,7 +38,7 @@ export function getUsersInGroup(data: {
ps?: number;
q?: string;
selected?: string;
}): Promise<T.Paging & { users: GroupUser[] }> {
}): Promise<T.Paging & { users: T.UserSelected[] }> {
return getJSON('/api/user_groups/users', data).catch(throwGlobalError);
}


+ 53
- 0
server/sonar-web/src/main/js/app/components/AccountDeleted.tsx 查看文件

@@ -0,0 +1,53 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate } from 'sonar-ui-common/helpers/l10n';

export default function AccountDeleted() {
return (
<div className="page-wrapper-simple display-flex-column">
<Alert className="huge-spacer-bottom" variant="success">
{translate('my_profile.delete_account.success')}
</Alert>

<div className="page-simple text-center">
<p className="big-spacer-bottom">
<h1>{translate('my_profile.delete_account.feedback.reason.explanation')}</h1>
</p>
<p className="spacer-bottom">
<FormattedMessage
defaultMessage={translate('my_profile.delete_account.feedback.call_to_action')}
id="my_profile.delete_account.feedback.call_to_action"
values={{
link: <Link to="/about/contact">{translate('footer.contact_us')}</Link>
}}
/>
</p>
<p>
<Link to="/">{translate('go_back_to_homepage')}</Link>
</p>
</div>
</div>
);
}

+ 27
- 0
server/sonar-web/src/main/js/app/components/__tests__/AccountDeleted-test.tsx 查看文件

@@ -0,0 +1,27 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { shallow } from 'enzyme';
import AccountDeleted from '../AccountDeleted';

it('should render correctly', () => {
expect(shallow(<AccountDeleted />)).toMatchSnapshot();
});

+ 53
- 0
server/sonar-web/src/main/js/app/components/__tests__/__snapshots__/AccountDeleted-test.tsx.snap 查看文件

@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div
className="page-wrapper-simple display-flex-column"
>
<Alert
className="huge-spacer-bottom"
variant="success"
>
my_profile.delete_account.success
</Alert>
<div
className="page-simple text-center"
>
<p
className="big-spacer-bottom"
>
<h1>
my_profile.delete_account.feedback.reason.explanation
</h1>
</p>
<p
className="spacer-bottom"
>
<FormattedMessage
defaultMessage="my_profile.delete_account.feedback.call_to_action"
id="my_profile.delete_account.feedback.call_to_action"
values={
Object {
"link": <Link
onlyActiveOnIndex={false}
style={Object {}}
to="/about/contact"
>
footer.contact_us
</Link>,
}
}
/>
</p>
<p>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/"
>
go_back_to_homepage
</Link>
</p>
</div>
</div>
`;

+ 4
- 0
server/sonar-web/src/main/js/app/styles/init/lists.css 查看文件

@@ -28,6 +28,10 @@ ul {
padding-left: 40px;
}

.list-styled.no-padding {
padding-left: calc(var(--gridSize) * 2);
}

ul.list-styled {
list-style: disc;
}

+ 39
- 27
server/sonar-web/src/main/js/app/types.d.ts 查看文件

@@ -239,12 +239,7 @@ declare namespace T {
};
projectKey: string;
pending?: boolean;
user: {
active?: boolean;
email?: string;
login: string;
name: string;
};
user: T.UserBase;
value: string;
updatedAt?: string;
}
@@ -333,7 +328,7 @@ declare namespace T {
export interface Issue {
actions: string[];
assignee?: string;
assigneeActive?: string;
assigneeActive?: boolean;
assigneeAvatar?: string;
assigneeLogin?: string;
assigneeName?: string;
@@ -374,6 +369,21 @@ declare namespace T {
type: T.IssueType;
}

export interface IssueChangelog {
avatar?: string;
creationDate: string;
diffs: IssueChangelogDiff[];
user: string;
isUserActive: boolean;
userName: string;
}

export interface IssueChangelogDiff {
key: string;
newValue?: string;
oldValue?: string;
}

export interface IssueComment {
author?: string;
authorActive?: boolean;
@@ -423,17 +433,14 @@ declare namespace T {
open?: boolean;
}

export interface LoggedInUser extends CurrentUser {
avatar?: string;
email?: string;
export interface LoggedInUser extends CurrentUser, UserActive {
externalIdentity?: string;
externalProvider?: string;
groups: string[];
homepage?: HomePage;
isLoggedIn: true;
local?: boolean;
login: string;
name: string;
personalOrganization?: string;
scmAccounts: string[];
settings?: CurrentUserSetting[];
}
@@ -527,10 +534,7 @@ declare namespace T {
url?: string;
}

export interface OrganizationMember {
login: string;
name: string;
avatar?: string;
export interface OrganizationMember extends UserActive {
groupCount?: number;
}

@@ -590,11 +594,7 @@ declare namespace T {
permissions: string[];
}

export interface PermissionUser {
avatar?: string;
email?: string;
login: string;
name: string;
export interface PermissionUser extends UserActive {
permissions: string[];
}

@@ -987,21 +987,33 @@ declare namespace T {
endOffset: number;
}

export interface User {
active: boolean;
avatar?: string;
email?: string;
export interface User extends UserBase {
externalIdentity?: string;
externalProvider?: string;
groups?: string[];
lastConnectionDate?: string;
local: boolean;
login: string;
name: string;
scmAccounts?: string[];
tokensCount?: number;
}

export interface UserActive extends UserBase {
active?: true;
name: string;
}

export interface UserBase {
active?: boolean;
avatar?: string;
email?: string;
login: string;
name?: string;
}

export interface UserSelected extends UserActive {
selected: boolean;
}

export interface UserToken {
name: string;
createdAt: string;

+ 5
- 1
server/sonar-web/src/main/js/app/utils/startReactApp.tsx 查看文件

@@ -20,7 +20,7 @@
/* eslint-disable react/jsx-sort-props */
import * as React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute, Redirect, RouteProps, RouteConfig } from 'react-router';
import { IndexRoute, Redirect, Route, RouteConfig, RouteProps, Router } from 'react-router';
import { Provider } from 'react-redux';
import { IntlProvider } from 'react-intl';
import { Location } from 'history';
@@ -202,6 +202,10 @@ export default function startReactApp(
import('../../apps/feedback/downgrade/DowngradeFeedback')
)}
/>
<Route
path="account-deleted"
component={lazyLoad(() => import('../components/AccountDeleted'))}
/>
</>
)}
<RouteWithChildRoutes path="organizations" childRoutes={organizationsRoutes} />

+ 9
- 0
server/sonar-web/src/main/js/apps/account/profile/Profile.tsx 查看文件

@@ -20,6 +20,7 @@
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import UserExternalIdentity from './UserExternalIdentity';
import UserDeleteAccount from './UserDeleteAccount';
import UserGroups from './UserGroups';
import UserScmAccounts from './UserScmAccounts';
import { isSonarCloud } from '../../../helpers/system';
@@ -59,6 +60,14 @@ export function Profile({ currentUser }: Props) {
<hr />

<UserScmAccounts scmAccounts={currentUser.scmAccounts} user={currentUser} />

{isSonarCloud() && (
<>
<hr />

<UserDeleteAccount user={currentUser} />
</>
)}
</div>
</div>
);

+ 125
- 0
server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccount.tsx 查看文件

@@ -0,0 +1,125 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { Button } from 'sonar-ui-common/components/controls/buttons';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import UserDeleteAccountModal from './UserDeleteAccountModal';
import UserDeleteAccountContent from './UserDeleteAccountContent';
import { whenLoggedIn } from '../../../components/hoc/whenLoggedIn';
import { withUserOrganizations } from '../../../components/hoc/withUserOrganizations';
import { getOrganizationsThatPreventDeletion } from '../../../api/organizations';

interface Props {
user: T.LoggedInUser;
userOrganizations: T.Organization[];
}

interface State {
loading: boolean;
organizationsToTransferOrDelete: T.Organization[];
showModal: boolean;
}

export class UserDeleteAccount extends React.PureComponent<Props, State> {
mounted = false;

state: State = {
loading: true,
organizationsToTransferOrDelete: [],
showModal: false
};

componentDidMount() {
this.mounted = true;
this.fetchOrganizationsThatPreventDeletion();
}

componentWillUnmount() {
this.mounted = false;
}

fetchOrganizationsThatPreventDeletion = () => {
getOrganizationsThatPreventDeletion().then(
({ organizations }) => {
if (this.mounted) {
this.setState({
loading: false,
organizationsToTransferOrDelete: organizations
});
}
},
() => {}
);
};

toggleModal = () => {
if (this.mounted) {
this.setState(state => ({
showModal: !state.showModal
}));
}
};

render() {
const { user, userOrganizations } = this.props;
const { organizationsToTransferOrDelete, loading, showModal } = this.state;

const label = translate('my_profile.delete_account');

return (
<div>
<h2 className="spacer-bottom">{label}</h2>

<DeferredSpinner loading={loading} />

{!loading && (
<>
<UserDeleteAccountContent
className="list-styled no-padding big-spacer-top big-spacer-bottom"
organizationsSafeToDelete={userOrganizations}
organizationsToTransferOrDelete={organizationsToTransferOrDelete}
/>

<Button
className="button-red"
disabled={organizationsToTransferOrDelete.length > 0}
onClick={this.toggleModal}
type="button">
{translate('delete')}
</Button>

{showModal && (
<UserDeleteAccountModal
label={label}
organizationsSafeToDelete={userOrganizations}
organizationsToTransferOrDelete={organizationsToTransferOrDelete}
toggleModal={this.toggleModal}
user={user}
/>
)}
</>
)}
</div>
);
}
}

export default whenLoggedIn(withUserOrganizations(UserDeleteAccount));

+ 148
- 0
server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountContent.tsx 查看文件

@@ -0,0 +1,148 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { getOrganizationUrl } from '../../../helpers/urls';

function getOrganizationLink(org: T.Organization, i: number, organizations: T.Organization[]) {
return (
<span key={org.key}>
<Link to={getOrganizationUrl(org.key)}>{org.name}</Link>
{i < organizations.length - 1 && ', '}
</span>
);
}

export function ShowOrganizationsToTransferOrDelete({
organizations
}: {
organizations: T.Organization[];
}) {
return (
<>
<p className="big-spacer-bottom">
<FormattedMessage
defaultMessage={translate('my_profile.delete_account.info.orgs_to_transfer_or_delete')}
id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
values={{
organizations: <>{organizations.map(getOrganizationLink)}</>
}}
/>
</p>

<Alert className="big-spacer-bottom" variant="warning">
<FormattedMessage
defaultMessage={translate(
'my_profile.delete_account.info.orgs_to_transfer_or_delete.info'
)}
id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
values={{
link: (
<a
href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
rel="noopener noreferrer"
target="_blank">
{translate('my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link')}
</a>
)
}}
/>
</Alert>
</>
);
}

export function ShowOrganizations({
className,
organizations
}: {
className?: string;
organizations: T.Organization[];
}) {
const organizationsIAdministrate = organizations.filter(o => o.actions && o.actions.admin);

return (
<ul className={className}>
<li className="spacer-bottom">{translate('my_profile.delete_account.info')}</li>

<li className="spacer-bottom">
<FormattedMessage
defaultMessage={translate('my_profile.delete_account.data.info')}
id="my_profile.delete_account.data.info"
values={{
help: (
<a
href="/documentation/user-guide/user-account/#delete-your-user-account"
rel="noopener noreferrer"
target="_blank">
{translate('learn_more')}
</a>
)
}}
/>
</li>

{organizations.length > 0 && (
<li className="spacer-bottom">
<FormattedMessage
defaultMessage={translate('my_profile.delete_account.info.orgs.members')}
id="my_profile.delete_account.info.orgs.members"
values={{
organizations: <>{organizations.map(getOrganizationLink)}</>
}}
/>
</li>
)}

{organizationsIAdministrate.length > 0 && (
<li className="spacer-bottom">
<FormattedMessage
defaultMessage={translate('my_profile.delete_account.info.orgs.administrators')}
id="my_profile.delete_account.info.orgs.administrators"
values={{
organizations: <>{organizationsIAdministrate.map(getOrganizationLink)}</>
}}
/>
</li>
)}
</ul>
);
}

interface UserDeleteAccountContentProps {
className?: string;
organizationsSafeToDelete: T.Organization[];
organizationsToTransferOrDelete: T.Organization[];
}

export default function UserDeleteAccountContent({
className,
organizationsSafeToDelete,
organizationsToTransferOrDelete
}: UserDeleteAccountContentProps) {
if (organizationsToTransferOrDelete.length > 0) {
return <ShowOrganizationsToTransferOrDelete organizations={organizationsToTransferOrDelete} />;
}

return <ShowOrganizations className={className} organizations={organizationsSafeToDelete} />;
}

+ 150
- 0
server/sonar-web/src/main/js/apps/account/profile/UserDeleteAccountModal.tsx 查看文件

@@ -0,0 +1,150 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { FormikProps } from 'formik';
import { connect } from 'react-redux';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { Alert } from 'sonar-ui-common/components/ui/Alert';
import InputValidationField from 'sonar-ui-common/components/controls/InputValidationField';
import UserDeleteAccountContent from './UserDeleteAccountContent';
import RecentHistory from '../../../app/components/RecentHistory';
import ValidationModal from '../../../components/controls/ValidationModal';
import { deactivateUser } from '../../../api/users';
import { Router, withRouter } from '../../../components/hoc/withRouter';
import { doLogout } from '../../../store/rootActions';

interface Values {
login: string;
}

interface DeleteModalProps {
doLogout: () => Promise<void>;
label: string;
organizationsSafeToDelete: T.Organization[];
organizationsToTransferOrDelete: T.Organization[];
router: Pick<Router, 'push'>;
toggleModal: VoidFunction;
user: T.LoggedInUser;
}

export class UserDeleteAccountModal extends React.PureComponent<DeleteModalProps> {
handleSubmit = () => {
const { user } = this.props;

return deactivateUser({ login: user.login })
.then(this.props.doLogout)
.then(() => {
RecentHistory.clear();
window.location.replace('/account-deleted');
});
};

handleValidate = ({ login }: Values) => {
const { user } = this.props;
const errors: { login?: string } = {};
const trimmedLogin = login.trim();

if (!trimmedLogin) {
errors.login = translate('my_profile.delete_account.login.required');
} else if (user.externalIdentity && trimmedLogin !== user.externalIdentity.trim()) {
errors.login = translate('my_profile.delete_account.login.wrong_value');
}

return errors;
};

render() {
const {
label,
organizationsSafeToDelete,
organizationsToTransferOrDelete,
toggleModal,
user
} = this.props;

return (
<ValidationModal
confirmButtonText={translate('delete')}
header={translateWithParameters(
'my_profile.delete_account.modal.header',
label,
user.externalIdentity || ''
)}
initialValues={{
login: ''
}}
isDestructive={true}
onClose={toggleModal}
onSubmit={this.handleSubmit}
validate={this.handleValidate}>
{({
dirty,
errors,
handleBlur,
handleChange,
isSubmitting,
touched,
values
}: FormikProps<Values>) => (
<>
<Alert className="big-spacer-bottom" variant="error">
{translate('my_profile.warning_message')}
</Alert>

<UserDeleteAccountContent
className="list-styled no-padding big-spacer-bottom"
organizationsSafeToDelete={organizationsSafeToDelete}
organizationsToTransferOrDelete={organizationsToTransferOrDelete}
/>

<InputValidationField
autoFocus={true}
dirty={dirty}
disabled={isSubmitting}
error={errors.login}
id="user-login"
label={
<label htmlFor="user-login">
{translate('my_profile.delete_account.verify')}
<em className="mandatory">*</em>
</label>
}
name="login"
onBlur={handleBlur}
onChange={handleChange}
touched={touched.login}
type="text"
value={values.login}
/>
</>
)}
</ValidationModal>
);
}
}

const mapStateToProps = () => ({});

const mapDispatchToProps = { doLogout: doLogout as any };

export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(UserDeleteAccountModal));

+ 87
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccount-test.tsx 查看文件

@@ -0,0 +1,87 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 { shallow } from 'enzyme';
import * as React from 'react';
import { click, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { mockLoggedInUser, mockOrganization } from '../../../../helpers/testMocks';
import { UserDeleteAccount } from '../UserDeleteAccount';
import { getOrganizationsThatPreventDeletion } from '../../../../api/organizations';

jest.mock('../../../../api/organizations', () => ({
getOrganizationsThatPreventDeletion: jest.fn().mockResolvedValue({ organizations: [] })
}));

beforeEach(() => {
jest.clearAllMocks();
});

const organizationToTransferOrDelete = {
key: 'luke-leia',
name: 'Luke and Leia'
};

it('should render correctly', async () => {
const wrapper = shallowRender();
expect(wrapper).toMatchSnapshot();

await waitAndUpdate(wrapper);
expect(wrapper).toMatchSnapshot();

click(wrapper.find('Button'));
expect(wrapper).toMatchSnapshot();
});

it('should get some organizations', async () => {
(getOrganizationsThatPreventDeletion as jest.Mock).mockResolvedValue({
organizations: [organizationToTransferOrDelete]
});

const wrapper = shallowRender();

await waitAndUpdate(wrapper);

expect(wrapper.state('loading')).toBeFalsy();
expect(wrapper.state('organizationsToTransferOrDelete')).toEqual([
organizationToTransferOrDelete
]);
expect(getOrganizationsThatPreventDeletion).toBeCalled();
expect(wrapper.find('Button').prop('disabled')).toBe(true);
});

it('should toggle modal', () => {
const wrapper = shallowRender();
wrapper.setState({ loading: false });
expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(false);
click(wrapper.find('Button'));
expect(wrapper.find('Connect(withRouter(UserDeleteAccountModal))').exists()).toBe(true);
});

function shallowRender(props: Partial<UserDeleteAccount['props']> = {}) {
const user = mockLoggedInUser({ externalIdentity: 'luke' });

const userOrganizations = [
mockOrganization({ key: 'luke-leia', name: 'Luke and Leia' }),
mockOrganization({ key: 'luke', name: 'Luke Skywalker' })
];

return shallow<UserDeleteAccount>(
<UserDeleteAccount user={user} userOrganizations={userOrganizations} {...props} />
);
}

+ 71
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountContent-test.tsx 查看文件

@@ -0,0 +1,71 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { shallow } from 'enzyme';
import UserDeleteAccountContent, {
ShowOrganizations,
ShowOrganizationsToTransferOrDelete
} from '../UserDeleteAccountContent';

const organizationSafeToDelete = {
key: 'luke',
name: 'Luke Skywalker'
};

const organizationToTransferOrDelete = {
key: 'luke-leia',
name: 'Luke and Leia'
};

it('should render content correctly', () => {
expect(
shallow(
<UserDeleteAccountContent
className="my-class"
organizationsSafeToDelete={[organizationSafeToDelete]}
organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
/>
)
).toMatchSnapshot();

expect(
shallow(
<UserDeleteAccountContent
className="my-class"
organizationsSafeToDelete={[organizationSafeToDelete]}
organizationsToTransferOrDelete={[]}
/>
)
).toMatchSnapshot();
});

it('should render correctly ShowOrganizationsToTransferOrDelete', () => {
expect(
shallow(
<ShowOrganizationsToTransferOrDelete organizations={[organizationToTransferOrDelete]} />
)
).toMatchSnapshot();
});

it('should render correctly ShowOrganizations', () => {
expect(
shallow(<ShowOrganizations organizations={[organizationSafeToDelete]} />)
).toMatchSnapshot();
});

+ 86
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/UserDeleteAccountModal-test.tsx 查看文件

@@ -0,0 +1,86 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { shallow } from 'enzyme';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { mockLoggedInUser, mockRouter } from '../../../../helpers/testMocks';
import { UserDeleteAccountModal } from '../UserDeleteAccountModal';
import { deactivateUser } from '../../../../api/users';

jest.mock('../../../../api/users', () => ({
deactivateUser: jest.fn()
}));

const organizationSafeToDelete = {
key: 'luke',
name: 'Luke Skywalker'
};

const organizationToTransferOrDelete = {
key: 'luke-leia',
name: 'Luke and Leia'
};

it('should render modal correctly', () => {
expect(shallowRender()).toMatchSnapshot();
});

it('should handle submit', async () => {
(deactivateUser as jest.Mock).mockResolvedValue(true);
window.location.replace = jest.fn();

const wrapper = shallowRender();
const instance = wrapper.instance();

instance.handleSubmit();
await waitAndUpdate(wrapper);

expect(deactivateUser).toBeCalled();
expect(window.location.replace).toHaveBeenCalledWith('/account-deleted');
});

it('should validate user input', () => {
const wrapper = shallowRender();
const instance = wrapper.instance();
const { handleValidate } = instance;

expect(handleValidate({ login: '' }).login).toBe('my_profile.delete_account.login.required');
expect(handleValidate({ login: 'abc' }).login).toBe(
'my_profile.delete_account.login.wrong_value'
);
expect(handleValidate({ login: 'luke' }).login).toBeUndefined();
});

function shallowRender(props: Partial<UserDeleteAccountModal['props']> = {}) {
const user = mockLoggedInUser({ externalIdentity: 'luke' });

return shallow<UserDeleteAccountModal>(
<UserDeleteAccountModal
doLogout={jest.fn().mockResolvedValue(true)}
label="label"
organizationsSafeToDelete={[organizationSafeToDelete]}
organizationsToTransferOrDelete={[organizationToTransferOrDelete]}
router={mockRouter()}
toggleModal={jest.fn()}
user={user}
{...props}
/>
);
}

+ 118
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccount-test.tsx.snap 查看文件

@@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render correctly 1`] = `
<div>
<h2
className="spacer-bottom"
>
my_profile.delete_account
</h2>
<DeferredSpinner
loading={true}
timeout={100}
/>
</div>
`;

exports[`should render correctly 2`] = `
<div>
<h2
className="spacer-bottom"
>
my_profile.delete_account
</h2>
<DeferredSpinner
loading={false}
timeout={100}
/>
<UserDeleteAccountContent
className="list-styled no-padding big-spacer-top big-spacer-bottom"
organizationsSafeToDelete={
Array [
Object {
"key": "luke-leia",
"name": "Luke and Leia",
},
Object {
"key": "luke",
"name": "Luke Skywalker",
},
]
}
organizationsToTransferOrDelete={Array []}
/>
<Button
className="button-red"
disabled={false}
onClick={[Function]}
type="button"
>
delete
</Button>
</div>
`;

exports[`should render correctly 3`] = `
<div>
<h2
className="spacer-bottom"
>
my_profile.delete_account
</h2>
<DeferredSpinner
loading={false}
timeout={100}
/>
<UserDeleteAccountContent
className="list-styled no-padding big-spacer-top big-spacer-bottom"
organizationsSafeToDelete={
Array [
Object {
"key": "luke-leia",
"name": "Luke and Leia",
},
Object {
"key": "luke",
"name": "Luke Skywalker",
},
]
}
organizationsToTransferOrDelete={Array []}
/>
<Button
className="button-red"
disabled={false}
onClick={[Function]}
type="button"
>
delete
</Button>
<Connect(withRouter(UserDeleteAccountModal))
label="my_profile.delete_account"
organizationsSafeToDelete={
Array [
Object {
"key": "luke-leia",
"name": "Luke and Leia",
},
Object {
"key": "luke",
"name": "Luke Skywalker",
},
]
}
organizationsToTransferOrDelete={Array []}
toggleModal={[Function]}
user={
Object {
"externalIdentity": "luke",
"groups": Array [],
"isLoggedIn": true,
"login": "luke",
"name": "Skywalker",
"scmAccounts": Array [],
}
}
/>
</div>
`;

+ 128
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountContent-test.tsx.snap 查看文件

@@ -0,0 +1,128 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render content correctly 1`] = `
<ShowOrganizationsToTransferOrDelete
organizations={
Array [
Object {
"key": "luke-leia",
"name": "Luke and Leia",
},
]
}
/>
`;

exports[`should render content correctly 2`] = `
<ShowOrganizations
className="my-class"
organizations={
Array [
Object {
"key": "luke",
"name": "Luke Skywalker",
},
]
}
/>
`;

exports[`should render correctly ShowOrganizations 1`] = `
<ul>
<li
className="spacer-bottom"
>
my_profile.delete_account.info
</li>
<li
className="spacer-bottom"
>
<FormattedMessage
defaultMessage="my_profile.delete_account.data.info"
id="my_profile.delete_account.data.info"
values={
Object {
"help": <a
href="/documentation/user-guide/user-account/#delete-your-user-account"
rel="noopener noreferrer"
target="_blank"
>
learn_more
</a>,
}
}
/>
</li>
<li
className="spacer-bottom"
>
<FormattedMessage
defaultMessage="my_profile.delete_account.info.orgs.members"
id="my_profile.delete_account.info.orgs.members"
values={
Object {
"organizations": <React.Fragment>
<span>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/organizations/luke"
>
Luke Skywalker
</Link>
</span>
</React.Fragment>,
}
}
/>
</li>
</ul>
`;

exports[`should render correctly ShowOrganizationsToTransferOrDelete 1`] = `
<Fragment>
<p
className="big-spacer-bottom"
>
<FormattedMessage
defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete"
id="my_profile.delete_account.info.orgs_to_transfer_or_delete"
values={
Object {
"organizations": <React.Fragment>
<span>
<Link
onlyActiveOnIndex={false}
style={Object {}}
to="/organizations/luke-leia"
>
Luke and Leia
</Link>
</span>
</React.Fragment>,
}
}
/>
</p>
<Alert
className="big-spacer-bottom"
variant="warning"
>
<FormattedMessage
defaultMessage="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
id="my_profile.delete_account.info.orgs_to_transfer_or_delete.info"
values={
Object {
"link": <a
href="https://sieg.eu.ngrok.io/documentation/organizations/overview/#how-to-transfer-ownership-of-an-organization"
rel="noopener noreferrer"
target="_blank"
>
my_profile.delete_account.info.orgs_to_transfer_or_delete.info.link
</a>,
}
}
/>
</Alert>
</Fragment>
`;

+ 19
- 0
server/sonar-web/src/main/js/apps/account/profile/__tests__/__snapshots__/UserDeleteAccountModal-test.tsx.snap 查看文件

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should render modal correctly 1`] = `
<ValidationModal
confirmButtonText="delete"
header="my_profile.delete_account.modal.header.label.luke"
initialValues={
Object {
"login": "",
}
}
isDestructive={true}
onClose={[MockFunction]}
onSubmit={[Function]}
validate={[Function]}
>
<Component />
</ValidationModal>
`;

+ 7
- 2
server/sonar-web/src/main/js/apps/custom-measures/components/Item.tsx 查看文件

@@ -18,7 +18,7 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import Tooltip from 'sonar-ui-common/components/controls/Tooltip';
import { formatMeasure } from 'sonar-ui-common/helpers/measures';
import ActionsDropdown, {
@@ -28,6 +28,7 @@ import ActionsDropdown, {
import DeleteForm from './DeleteForm';
import Form from './Form';
import MeasureDate from './MeasureDate';
import { isUserActive } from '../../../helpers/users';

interface Props {
measure: T.CustomMeasure;
@@ -114,7 +115,11 @@ export default class Item extends React.PureComponent<Props, State> {

<td>
<MeasureDate measure={measure} /> {translate('by_')}{' '}
<span className="js-custom-measure-user">{measure.user.name}</span>
<span className="js-custom-measure-user">
{isUserActive(measure.user)
? measure.user.name || measure.user.login
: translateWithParameters('user.x_deleted', measure.user.login)}
</span>
</td>

<td className="thin nowrap">

+ 13
- 5
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/Item-test.tsx 查看文件

@@ -33,14 +33,12 @@ const measure = {
};

it('should render', () => {
expect(
shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} />)
).toMatchSnapshot();
expect(shallowRender()).toMatchSnapshot();
});

it('should edit metric', () => {
const onEdit = jest.fn();
const wrapper = shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={onEdit} />);
const wrapper = shallowRender({ onEdit });

click(wrapper.find('.js-custom-measure-update'));
wrapper.update();
@@ -55,7 +53,7 @@ it('should edit metric', () => {

it('should delete custom measure', () => {
const onDelete = jest.fn();
const wrapper = shallow(<Item measure={measure} onDelete={onDelete} onEdit={jest.fn()} />);
const wrapper = shallowRender({ onDelete });

click(wrapper.find('.js-custom-measure-delete'));
wrapper.update();
@@ -63,3 +61,13 @@ it('should delete custom measure', () => {
wrapper.find('DeleteForm').prop<Function>('onSubmit')();
expect(onDelete).toBeCalledWith('1');
});

it('should render correctly for deleted user', () => {
expect(
shallowRender({ measure: { ...measure, user: { active: false, login: 'user' } } })
).toMatchSnapshot();
});

function shallowRender(props: Partial<Item['props']> = {}) {
return shallow(<Item measure={measure} onDelete={jest.fn()} onEdit={jest.fn()} {...props} />);
}

+ 87
- 0
server/sonar-web/src/main/js/apps/custom-measures/components/__tests__/__snapshots__/Item-test.tsx.snap 查看文件

@@ -87,3 +87,90 @@ exports[`should render 1`] = `
</td>
</tr>
`;

exports[`should render correctly for deleted user 1`] = `
<tr
data-metric="custom"
>
<td
className="nowrap"
>
<div>
<span
className="js-custom-measure-metric-name"
>
custom-metric
</span>
</div>
<span
className="js-custom-measure-domain note"
/>
</td>
<td
className="nowrap"
>
<strong
className="js-custom-measure-value"
>
custom-value
</strong>
</td>
<td>
<span
className="js-custom-measure-description"
>
my custom measure
</span>
</td>
<td>
<MeasureDate
measure={
Object {
"createdAt": "2017-01-01",
"description": "my custom measure",
"id": "1",
"metric": Object {
"key": "custom",
"name": "custom-metric",
"type": "STRING",
},
"projectKey": "foo",
"user": Object {
"active": false,
"login": "user",
},
"value": "custom-value",
}
}
/>
by_
<span
className="js-custom-measure-user"
>
user.x_deleted.user
</span>
</td>
<td
className="thin nowrap"
>
<ActionsDropdown>
<ActionsDropdownItem
className="js-custom-measure-update"
onClick={[Function]}
>
update_verb
</ActionsDropdownItem>
<ActionsDropdownDivider />
<ActionsDropdownItem
className="js-custom-measure-delete"
destructive={true}
onClick={[Function]}
>
delete
</ActionsDropdownItem>
</ActionsDropdown>
</td>
</tr>
`;

+ 2
- 7
server/sonar-web/src/main/js/apps/groups/components/EditMembersModal.tsx 查看文件

@@ -24,12 +24,7 @@ import { ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
import SelectList, { Filter } from '../../../components/SelectList/SelectList';
import {
addUserToGroup,
getUsersInGroup,
GroupUser,
removeUserFromGroup
} from '../../../api/user_groups';
import { addUserToGroup, getUsersInGroup, removeUserFromGroup } from '../../../api/user_groups';

interface Props {
group: T.Group;
@@ -50,7 +45,7 @@ interface State {
lastSearchParams: SearchParams;
listHasBeenTouched: boolean;
loading: boolean;
users: GroupUser[];
users: T.UserSelected[];
usersTotalCount?: number;
selectedUsers: string[];
}

+ 2
- 3
server/sonar-web/src/main/js/apps/issues/components/App.tsx 查看文件

@@ -66,7 +66,6 @@ import {
ReferencedComponent,
ReferencedLanguage,
ReferencedRule,
ReferencedUser,
saveMyIssues,
serializeQuery,
STANDARDS,
@@ -96,7 +95,7 @@ interface FetchIssuesPromise {
languages: ReferencedLanguage[];
paging: T.Paging;
rules: ReferencedRule[];
users: ReferencedUser[];
users: T.UserBase[];
}

interface Props {
@@ -136,7 +135,7 @@ export interface State {
referencedComponentsByKey: T.Dict<ReferencedComponent>;
referencedLanguages: T.Dict<ReferencedLanguage>;
referencedRules: T.Dict<ReferencedRule>;
referencedUsers: T.Dict<ReferencedUser>;
referencedUsers: T.Dict<T.UserBase>;
selected?: string;
selectedFlowIndex?: number;
selectedLocationIndex?: number;

+ 6
- 2
server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx 查看文件

@@ -36,7 +36,7 @@ import Select from '../../../components/controls/Select';
import SeverityHelper from '../../../components/shared/SeverityHelper';
import throwGlobalError from '../../../app/utils/throwGlobalError';
import { searchIssueTags, bulkChangeIssues } from '../../../api/issues';
import { isLoggedIn } from '../../../helpers/users';
import { isLoggedIn, isUserActive } from '../../../helpers/users';

interface AssigneeOption {
avatar?: string;
@@ -161,7 +161,11 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> {

handleAssigneeSearch = (query: string) => {
return searchAssignees(query, this.state.organization).then(({ results }) =>
results.map(r => ({ avatar: r.avatar, label: r.name, value: r.login }))
results.map(r => ({
avatar: r.avatar,
label: isUserActive(r) ? r.name : translateWithParameters('user.x_deleted', r.login),
value: r.login
}))
);
};


+ 31
- 20
server/sonar-web/src/main/js/apps/issues/sidebar/AssigneeFacet.tsx 查看文件

@@ -20,12 +20,13 @@
import * as React from 'react';
import { omit, sortBy, without } from 'lodash';
import { highlightTerm } from 'sonar-ui-common/helpers/search';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { searchAssignees, Query, ReferencedUser, SearchedAssignee, Facet } from '../utils';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { searchAssignees, Query, Facet } from '../utils';
import Avatar from '../../../components/ui/Avatar';
import ListStyleFacet from '../../../components/facet/ListStyleFacet';
import { isUserActive } from '../../../helpers/users';

export interface Props {
interface Props {
assigned: boolean;
assignees: string[];
fetching: boolean;
@@ -36,7 +37,7 @@ export interface Props {
organization: string | undefined;
query: Query;
stats: T.Dict<number> | undefined;
referencedUsers: T.Dict<ReferencedUser>;
referencedUsers: T.Dict<T.UserBase>;
}

export default class AssigneeFacet extends React.PureComponent<Props> {
@@ -71,11 +72,14 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
return translate('unassigned');
} else {
const user = this.props.referencedUsers[assignee];
return user ? user.name : assignee;
if (!user) {
return assignee;
}
return isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login);
}
};

loadSearchResultCount = (assignees: SearchedAssignee[]) => {
loadSearchResultCount = (assignees: T.UserBase[]) => {
return this.props.loadSearchResultCount('assignees', {
assigned: undefined,
assignees: assignees.map(assignee => assignee.login)
@@ -99,28 +103,35 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}

const user = this.props.referencedUsers[assignee];

return user ? (
<>
<Avatar className="little-spacer-right" hash={user.avatar} name={user.name} size={16} />
{user.name}
<Avatar
className="little-spacer-right"
hash={user.avatar}
name={user.name || user.login}
size={16}
/>
{isUserActive(user) ? user.name : translateWithParameters('user.x_deleted', user.login)}
</>
) : (
assignee
);
};

renderSearchResult = (result: SearchedAssignee, query: string) => {
renderSearchResult = (result: T.UserBase, query: string) => {
const displayName = isUserActive(result)
? result.name
: translateWithParameters('user.x_deleted', result.login);
return (
<>
{result.avatar !== undefined && (
<Avatar
className="little-spacer-right"
hash={result.avatar}
name={result.name}
size={16}
/>
)}
{highlightTerm(result.name, query)}
<Avatar
className="little-spacer-right"
hash={result.avatar}
name={result.name || result.login}
size={16}
/>
{highlightTerm(displayName, query)}
</>
);
};
@@ -132,12 +143,12 @@ export default class AssigneeFacet extends React.PureComponent<Props> {
}

return (
<ListStyleFacet<SearchedAssignee>
<ListStyleFacet<T.UserBase>
facetHeader={translate('issues.facet.assignees')}
fetching={this.props.fetching}
getFacetItemText={this.getAssigneeName}
getSearchResultKey={user => user.login}
getSearchResultText={user => user.name}
getSearchResultText={user => user.name || user.login}
// put "not assigned" item first
getSortedItems={this.getSortedItems}
loadSearchResultCount={this.loadSearchResultCount}

+ 2
- 9
server/sonar-web/src/main/js/apps/issues/sidebar/Sidebar.tsx 查看文件

@@ -32,14 +32,7 @@ import StandardFacet from './StandardFacet';
import StatusFacet from './StatusFacet';
import TagFacet from './TagFacet';
import TypeFacet from './TypeFacet';
import {
Query,
Facet,
ReferencedComponent,
ReferencedUser,
ReferencedLanguage,
ReferencedRule
} from '../utils';
import { Query, Facet, ReferencedComponent, ReferencedLanguage, ReferencedRule } from '../utils';

export interface Props {
component: T.Component | undefined;
@@ -57,7 +50,7 @@ export interface Props {
referencedComponentsByKey: T.Dict<ReferencedComponent>;
referencedLanguages: T.Dict<ReferencedLanguage>;
referencedRules: T.Dict<ReferencedRule>;
referencedUsers: T.Dict<ReferencedUser>;
referencedUsers: T.Dict<T.UserBase>;
}

export default class Sidebar extends React.PureComponent<Props> {

+ 38
- 7
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/AssigneeFacet-test.tsx 查看文件

@@ -19,18 +19,18 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import AssigneeFacet, { Props } from '../AssigneeFacet';
import AssigneeFacet from '../AssigneeFacet';
import { Query } from '../../utils';

jest.mock('../../../../store/rootReducer', () => ({}));

it('should render', () => {
expect(renderAssigneeFacet({ assignees: ['foo'] })).toMatchSnapshot();
expect(shallowRender({ assignees: ['foo'] })).toMatchSnapshot();
});

it('should select unassigned', () => {
expect(
renderAssigneeFacet({ assigned: false })
shallowRender({ assigned: false })
.find('ListStyleFacet')
.prop('values')
).toEqual(['']);
@@ -38,7 +38,7 @@ it('should select unassigned', () => {

it('should call onChange', () => {
const onChange = jest.fn();
const wrapper = renderAssigneeFacet({ assignees: ['foo'], onChange });
const wrapper = shallowRender({ assignees: ['foo'], onChange });
const itemOnClick = wrapper.find('ListStyleFacet').prop<Function>('onItemClick');

itemOnClick('');
@@ -51,8 +51,39 @@ it('should call onChange', () => {
expect(onChange).lastCalledWith({ assigned: true, assignees: ['baz', 'foo'] });
});

function renderAssigneeFacet(props?: Partial<Props>) {
return shallow(
describe('test behavior', () => {
const instance = shallowRender({
assignees: ['foo', 'baz'],
referencedUsers: {
foo: { active: false, login: 'foo' },
baz: { active: true, login: 'baz', name: 'Name Baz' }
}
}).instance();

it('should correctly render assignee name', () => {
expect(instance.getAssigneeName('')).toBe('unassigned');
expect(instance.getAssigneeName('bar')).toBe('bar');
expect(instance.getAssigneeName('baz')).toBe('Name Baz');
expect(instance.getAssigneeName('foo')).toBe('user.x_deleted.foo');
});

it('should correctly render facet item', () => {
expect(instance.renderFacetItem('')).toBe('unassigned');
expect(instance.renderFacetItem('bar')).toBe('bar');
expect(instance.renderFacetItem('baz')).toMatchSnapshot();
expect(instance.renderFacetItem('foo')).toMatchSnapshot();
});

it('should correctly render search result correctly', () => {
expect(
instance.renderSearchResult({ active: true, login: 'bar', name: 'Name Bar' }, 'ba')
).toMatchSnapshot();
expect(instance.renderSearchResult({ active: false, login: 'foo' }, 'fo')).toMatchSnapshot();
});
});

function shallowRender(props?: Partial<AssigneeFacet['props']>) {
return shallow<AssigneeFacet>(
<AssigneeFacet
assigned={true}
assignees={[]}
@@ -63,7 +94,7 @@ function renderAssigneeFacet(props?: Partial<Props>) {
open={true}
organization={undefined}
query={{} as Query}
referencedUsers={{ foo: { avatar: 'avatart-foo', name: 'name-foo' } }}
referencedUsers={{ foo: { avatar: 'avatart-foo', login: 'name-foo', name: 'Name Foo' } }}
stats={{ '': 5, foo: 13, bar: 7, baz: 6 }}
{...props}
/>

+ 56
- 0
server/sonar-web/src/main/js/apps/issues/sidebar/__tests__/__snapshots__/AssigneeFacet-test.tsx.snap 查看文件

@@ -38,3 +38,59 @@ exports[`should render 1`] = `
}
/>
`;

exports[`test behavior should correctly render facet item 1`] = `
<React.Fragment>
<Connect(Avatar)
className="little-spacer-right"
name="Name Baz"
size={16}
/>
Name Baz
</React.Fragment>
`;

exports[`test behavior should correctly render facet item 2`] = `
<React.Fragment>
<Connect(Avatar)
className="little-spacer-right"
name="foo"
size={16}
/>
user.x_deleted.foo
</React.Fragment>
`;

exports[`test behavior should correctly render search result correctly 1`] = `
<React.Fragment>
<Connect(Avatar)
className="little-spacer-right"
name="Name Bar"
size={16}
/>
<React.Fragment>
Name
<mark>
Ba
</mark>
r
</React.Fragment>
</React.Fragment>
`;

exports[`test behavior should correctly render search result correctly 2`] = `
<React.Fragment>
<Connect(Avatar)
className="little-spacer-right"
name="foo"
size={16}
/>
<React.Fragment>
user.x_deleted.
<mark>
fo
</mark>
o
</React.Fragment>
</React.Fragment>
`;

+ 1
- 12
server/sonar-web/src/main/js/apps/issues/utils.ts 查看文件

@@ -199,11 +199,6 @@ export interface ReferencedComponent {
uuid: string;
}

export interface ReferencedUser {
avatar: string;
name: string;
}

export interface ReferencedLanguage {
name: string;
}
@@ -213,17 +208,11 @@ export interface ReferencedRule {
name: string;
}

export interface SearchedAssignee {
avatar?: string;
login: string;
name: string;
}

export const searchAssignees = (
query: string,
organization: string | undefined,
page = 1
): Promise<{ paging: T.Paging; results: SearchedAssignee[] }> => {
): Promise<{ paging: T.Paging; results: T.UserBase[] }> => {
return organization
? searchMembers({ organization, p: page, ps: 50, q: query }).then(({ paging, users }) => ({
paging,

+ 19
- 14
server/sonar-web/src/main/js/apps/projects/components/NoFavoriteProjects.tsx 查看文件

@@ -50,20 +50,25 @@ export function NoFavoriteProjects(props: StateProps & OwnProps) {
{translate('provisioning.analyze_new_project')}
</Button>

<Dropdown
className="display-inline-block big-spacer-left"
overlay={
<ul className="menu">
{sortBy(props.organizations, org => org.name.toLowerCase()).map(organization => (
<OrganizationListItem key={organization.key} organization={organization} />
))}
</ul>
}>
<a className="button" href="#">
{translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
<DropdownIcon className="little-spacer-left" />
</a>
</Dropdown>
{props.organizations.length > 0 && (
<Dropdown
className="display-inline-block big-spacer-left"
overlay={
<ul className="menu">
{sortBy(props.organizations, org => org.name.toLowerCase()).map(
organization => (
<OrganizationListItem key={organization.key} organization={organization} />
)
)}
</ul>
}>
<a className="button" href="#">
{translate('projects.no_favorite_projects.favorite_projects_from_orgs')}
<DropdownIcon className="little-spacer-left" />
</a>
</Dropdown>
)}

<Link className="button big-spacer-left" to="/explore/projects">
{translate('projects.no_favorite_projects.favorite_public_projects')}
</Link>

+ 8
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/NoFavoriteProjects-test.tsx 查看文件

@@ -31,7 +31,14 @@ it('renders', () => {
).toMatchSnapshot();
});

it('renders for SonarCloud', () => {
it('renders for SonarCloud without organizations', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
expect(
shallow(<NoFavoriteProjects openProjectOnboarding={jest.fn()} organizations={[]} />)
).toMatchSnapshot();
});

it('renders for SonarCloud with organizations', () => {
(isSonarCloud as jest.Mock).mockImplementation(() => true);
const organizations: T.Organization[] = [
{ actions: { admin: true }, key: 'org1', name: 'org1', projectVisibility: 'public' },

+ 35
- 1
server/sonar-web/src/main/js/apps/projects/components/__tests__/__snapshots__/NoFavoriteProjects-test.tsx.snap 查看文件

@@ -29,7 +29,7 @@ exports[`renders 1`] = `
</div>
`;

exports[`renders for SonarCloud 1`] = `
exports[`renders for SonarCloud with organizations 1`] = `
<div
className="projects-empty-list"
>
@@ -105,3 +105,37 @@ exports[`renders for SonarCloud 1`] = `
</div>
</div>
`;

exports[`renders for SonarCloud without organizations 1`] = `
<div
className="projects-empty-list"
>
<h3>
projects.no_favorite_projects
</h3>
<div
className="spacer-top"
>
<p>
projects.no_favorite_projects.how_to_add_projects
</p>
<div
className="huge-spacer-top"
>
<Button
onClick={[MockFunction]}
>
provisioning.analyze_new_project
</Button>
<Link
className="button big-spacer-left"
onlyActiveOnIndex={false}
style={Object {}}
to="/explore/projects"
>
projects.no_favorite_projects.favorite_public_projects
</Link>
</div>
</div>
</div>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/App.tsx 查看文件

@@ -31,7 +31,7 @@ import Suggestions from '../../app/components/embed-docs-modal/Suggestions';
import { getComponents, Project } from '../../api/components';

export interface Props {
currentUser: { login: string };
currentUser: Pick<T.LoggedInUser, 'login'>;
hasProvisionPermission?: boolean;
onOrganizationUpgrade: () => void;
onVisibilityChange: (visibility: T.Visibility) => void;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRow.tsx 查看文件

@@ -27,7 +27,7 @@ import DateTooltipFormatter from '../../components/intl/DateTooltipFormatter';
import { Project } from '../../api/components';

interface Props {
currentUser: { login: string };
currentUser: Pick<T.LoggedInUser, 'login'>;
onProjectCheck: (project: Project, checked: boolean) => void;
organization: string | undefined;
project: Project;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/ProjectRowActions.tsx 查看文件

@@ -29,7 +29,7 @@ import { getComponentNavigation } from '../../api/nav';
import { getComponentPermissionsUrl } from '../../helpers/urls';

export interface Props {
currentUser: { login: string };
currentUser: Pick<T.LoggedInUser, 'login'>;
organization: string | undefined;
project: Project;
}

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/Projects.tsx 查看文件

@@ -24,7 +24,7 @@ import ProjectRow from './ProjectRow';
import { Project } from '../../api/components';

interface Props {
currentUser: { login: string };
currentUser: Pick<T.LoggedInUser, 'login'>;
onProjectDeselected: (project: string) => void;
onProjectSelected: (project: string) => void;
organization: T.Organization;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectsManagement/RestoreAccessModal.tsx 查看文件

@@ -26,7 +26,7 @@ import { grantPermissionToUser } from '../../api/permissions';
import { Project } from '../../api/components';

interface Props {
currentUser: { login: string };
currentUser: Pick<T.LoggedInUser, 'login'>;
onClose: () => void;
onRestoreAccess: () => void;
project: Project;

+ 3
- 9
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissions.tsx 查看文件

@@ -31,12 +31,6 @@ import {
} from '../../../api/quality-profiles';
import { Profile } from '../types';

export interface User {
avatar?: string;
login: string;
name: string;
}

export interface Group {
name: string;
}
@@ -50,7 +44,7 @@ interface State {
addUserForm: boolean;
groups?: Group[];
loading: boolean;
users?: User[];
users?: T.UserSelected[];
}

export default class ProfilePermissions extends React.PureComponent<Props, State> {
@@ -112,7 +106,7 @@ export default class ProfilePermissions extends React.PureComponent<Props, State
}
};

handleUserAdd = (addedUser: User) => {
handleUserAdd = (addedUser: T.UserSelected) => {
if (this.mounted) {
this.setState((state: State) => ({
addUserForm: false,
@@ -121,7 +115,7 @@ export default class ProfilePermissions extends React.PureComponent<Props, State
}
};

handleUserDelete = (removedUser: User) => {
handleUserDelete = (removedUser: T.UserSelected) => {
if (this.mounted) {
this.setState((state: State) => ({
users: state.users && state.users.filter(user => user !== removedUser)

+ 7
- 7
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsForm.tsx 查看文件

@@ -21,8 +21,8 @@ import * as React from 'react';
import { translate } from 'sonar-ui-common/helpers/l10n';
import { SubmitButton, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons';
import Modal from 'sonar-ui-common/components/controls/Modal';
import { User, Group } from './ProfilePermissions';
import ProfilePermissionsFormSelect from './ProfilePermissionsFormSelect';
import { Group } from './ProfilePermissions';
import {
searchUsers,
searchGroups,
@@ -34,13 +34,13 @@ import {
interface Props {
onClose: () => void;
onGroupAdd: (group: Group) => void;
onUserAdd: (user: User) => void;
onUserAdd: (user: T.UserSelected) => void;
organization?: string;
profile: { language: string; name: string };
}

interface State {
selected?: User | Group;
selected?: T.UserSelected | Group;
submitting: boolean;
}

@@ -62,7 +62,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
}
};

handleUserAdd = (user: User) =>
handleUserAdd = (user: T.UserSelected) =>
addUser({
language: this.props.profile.language,
login: user.login,
@@ -83,8 +83,8 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
const { selected } = this.state;
if (selected) {
this.setState({ submitting: true });
if ((selected as User).login !== undefined) {
this.handleUserAdd(selected as User);
if ((selected as T.UserSelected).login !== undefined) {
this.handleUserAdd(selected as T.UserSelected);
} else {
this.handleGroupAdd(selected as Group);
}
@@ -105,7 +105,7 @@ export default class ProfilePermissionsForm extends React.PureComponent<Props, S
);
};

handleValueChange = (selected: User | Group) => {
handleValueChange = (selected: T.UserSelected | Group) => {
this.setState({ selected });
};


+ 4
- 4
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsFormSelect.tsx 查看文件

@@ -21,11 +21,11 @@ import * as React from 'react';
import GroupIcon from 'sonar-ui-common/components/icons/GroupIcon';
import { debounce, identity } from 'lodash';
import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n';
import { User, Group } from './ProfilePermissions';
import { Group } from './ProfilePermissions';
import Select from '../../../components/controls/Select';
import Avatar from '../../../components/ui/Avatar';

type Option = User | Group;
type Option = T.UserSelected | Group;
type OptionWithValue = Option & { value: string };

interface Props {
@@ -112,8 +112,8 @@ export default class ProfilePermissionsFormSelect extends React.PureComponent<Pr
}
}

function isUser(option: Option): option is User {
return (option as User).login !== undefined;
function isUser(option: Option): option is T.UserSelected {
return (option as T.UserSelected).login !== undefined;
}

function getStringValue(option: Option) {

+ 2
- 3
server/sonar-web/src/main/js/apps/quality-profiles/details/ProfilePermissionsUser.tsx 查看文件

@@ -26,15 +26,14 @@ import {
ResetButtonLink
} from 'sonar-ui-common/components/controls/buttons';
import SimpleModal, { ChildrenProps } from 'sonar-ui-common/components/controls/SimpleModal';
import { User } from './ProfilePermissions';
import { removeUser } from '../../../api/quality-profiles';
import Avatar from '../../../components/ui/Avatar';

interface Props {
onDelete: (user: User) => void;
onDelete: (user: T.UserSelected) => void;
organization?: string;
profile: { language: string; name: string };
user: User;
user: T.UserSelected;
}

interface State {

+ 7
- 9
server/sonar-web/src/main/js/apps/quality-profiles/details/__tests__/ProfilePermissionsUser-test.tsx 查看文件

@@ -17,23 +17,21 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
/* eslint-disable import/first, import/order */
jest.mock('../../../../api/quality-profiles', () => ({
removeUser: jest.fn(() => Promise.resolve())
}));

import * as React from 'react';
import { shallow } from 'enzyme';
import ProfilePermissionsUser from '../ProfilePermissionsUser';
import { click } from 'sonar-ui-common/helpers/testUtils';
import ProfilePermissionsUser from '../ProfilePermissionsUser';
import { removeUser } from '../../../../api/quality-profiles';

const removeUser = require('../../../../api/quality-profiles').removeUser as jest.Mock<any>;
jest.mock('../../../../api/quality-profiles', () => ({
removeUser: jest.fn(() => Promise.resolve())
}));

const profile = { language: 'js', name: 'Sonar way' };
const user = { login: 'luke', name: 'Luke Skywalker' };
const user: T.UserSelected = { login: 'luke', name: 'Luke Skywalker', selected: true };

beforeEach(() => {
removeUser.mockClear();
jest.clearAllMocks();
});

it('renders', () => {

+ 2
- 2
server/sonar-web/src/main/js/apps/sessions/components/Logout.tsx 查看文件

@@ -29,12 +29,12 @@ interface Props {
doLogout: () => Promise<void>;
}

class Logout extends React.PureComponent<Props> {
export class Logout extends React.PureComponent<Props> {
componentDidMount() {
this.props.doLogout().then(
() => {
RecentHistory.clear();
window.location.href = getBaseUrl() + '/';
window.location.replace(getBaseUrl() + '/');
},
() => {}
);

+ 50
- 0
server/sonar-web/src/main/js/apps/sessions/components/__tests__/Logout-test.tsx 查看文件

@@ -0,0 +1,50 @@
/*
* SonarQube
* Copyright (C) 2009-2019 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 * as React from 'react';
import { shallow } from 'enzyme';
import { waitAndUpdate } from 'sonar-ui-common/helpers/testUtils';
import { Logout } from '../Logout';

it('should logout correctly', async () => {
const doLogout = jest.fn().mockResolvedValue(true);
window.location.replace = jest.fn();

const wrapper = shallowRender({ doLogout });
await waitAndUpdate(wrapper);

expect(doLogout).toHaveBeenCalled();
expect(window.location.replace).toHaveBeenCalledWith('/');
});

it('should not redirect if logout fails', async () => {
const doLogout = jest.fn().mockRejectedValue(false);
window.location.replace = jest.fn();

const wrapper = shallowRender({ doLogout });
await waitAndUpdate(wrapper);

expect(doLogout).toHaveBeenCalled();
expect(window.location.replace).not.toHaveBeenCalled();
expect(wrapper).toMatchSnapshot();
});

function shallowRender(props: Partial<Logout['props']> = {}) {
return shallow(<Logout doLogout={jest.fn()} {...props} />);
}

+ 14
- 0
server/sonar-web/src/main/js/apps/sessions/components/__tests__/__snapshots__/Logout-test.tsx.snap 查看文件

@@ -0,0 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`should not redirect if logout fails 1`] = `
<div
className="page page-limited"
>
<Connect(GlobalMessages) />
<div
className="text-center"
>
logging_out
</div>
</div>
`;

+ 1
- 1
server/sonar-web/src/main/js/apps/users/components/DeactivateForm.tsx 查看文件

@@ -26,7 +26,7 @@ import { deactivateUser } from '../../../api/users';
export interface Props {
onClose: () => void;
onUpdateUsers: () => void;
user: T.User;
user: T.UserActive;
}

interface State {

+ 0
- 0
server/sonar-web/src/main/js/apps/users/components/UserActions.tsx 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存