]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11579 Create WS to store/load user settings
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 12 Dec 2018 09:16:11 +0000 (10:16 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 20 Dec 2018 10:41:52 +0000 (11:41 +0100)
37 files changed:
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
server/sonar-db-core/src/main/resources/org/sonar/db/version/schema-h2.ddl
server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesDao.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesMapper.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertyDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserPropertiesMapper.xml [new file with mode: 0644]
server/sonar-db-dao/src/test/java/org/sonar/db/DaoModuleTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDbTester.java
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserPropertiesDaoTest.java [new file with mode: 0644]
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserTesting.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MigrationConfigurationModule.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTable.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTable.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/package-info.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/MigrationConfigurationModuleTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v75/CreateOrganizationsAlmBindingsTableTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTableTest.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java [new file with mode: 0644]
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest/user_properties.sql [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/ws/CurrentAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/user/ws/UsersWsModule.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/CurrentActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetSettingRequest.java [new file with mode: 0644]
sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java
sonar-ws/src/main/protobuf/ws-users.proto

index 6e0262645515b203ee78b9eece08860d8e087fba..046bfd3d8c0a6f72f42cc6cb0d8e93eabe534ce9 100644 (file)
@@ -115,13 +115,13 @@ public class ComputeEngineContainerImplTest {
       );
       assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize(
         CONTAINER_ITSELF
-          + 19 // MigrationConfigurationModule
+          + 20 // MigrationConfigurationModule
           + 17 // level 2
       );
       assertThat(picoContainer.getParent().getParent().getParent().getComponentAdapters()).hasSize(
         COMPONENTS_IN_LEVEL_1_AT_CONSTRUCTION
           + 26 // level 1
-          + 59 // content of DaoModule
+          + 60 // content of DaoModule
           + 3 // content of EsModule
           + 54 // content of CorePropertyDefinitions
           + 1 // StopFlagContainer
index 1135f0403013e1a40d5552ad491477f8671a69ac..48708e80f747ac04b8b0a7b5c81ac8298a6f9249 100644 (file)
@@ -108,6 +108,7 @@ public final class SqTables {
     "schema_migrations",
     "snapshots",
     "users",
+    "user_properties",
     "user_roles",
     "user_tokens",
     "webhooks",
index e40e736765dbf5a646804d4a5fb93a1267c3933c..6790a438c54e09a2ac7903ea81ef0b57ebe98283 100644 (file)
@@ -936,3 +936,15 @@ CREATE TABLE "ORGANIZATION_ALM_BINDINGS" (
 );
 CREATE UNIQUE INDEX "ORG_ALM_BINDINGS_ORG" ON "ORGANIZATION_ALM_BINDINGS" ("ORGANIZATION_UUID");
 CREATE UNIQUE INDEX "ORG_ALM_BINDINGS_INSTALL" ON "ORGANIZATION_ALM_BINDINGS" ("ALM_APP_INSTALL_UUID");
+
+CREATE TABLE "USER_PROPERTIES" (
+  "UUID" VARCHAR(40) NOT NULL,
+  "USER_UUID" VARCHAR(255) NOT NULL,
+  "KEE" VARCHAR(100) NOT NULL,
+  "TEXT_VALUE" VARCHAR(4000) NOT NULL,
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL,
+
+  CONSTRAINT "PK_USER_PROPERTIES" PRIMARY KEY ("UUID")
+);
+CREATE UNIQUE INDEX "USER_PROPERTIES_USER_UUID_KEE" ON "USER_PROPERTIES" ("USER_UUID", "KEE");
index ae037109b9dfb8eeba40c68040717eae74f52b57..e392da3ab614e81bd657ecb1e4bfc6a0306c39fe 100644 (file)
@@ -79,6 +79,7 @@ import org.sonar.db.user.GroupMembershipDao;
 import org.sonar.db.user.RoleDao;
 import org.sonar.db.user.UserDao;
 import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserPropertiesDao;
 import org.sonar.db.user.UserTokenDao;
 import org.sonar.db.webhook.WebhookDao;
 import org.sonar.db.webhook.WebhookDeliveryDao;
@@ -144,6 +145,7 @@ public class DaoModule extends Module {
     UserDao.class,
     UserGroupDao.class,
     UserPermissionDao.class,
+    UserPropertiesDao.class,
     UserTokenDao.class,
     WebhookDao.class,
     WebhookDeliveryDao.class));
index 5d9bf7277bd7c3dca1be2e673af1d0caa1180847..2a056737912273676dc2eeb3549d3fc31088d48e 100644 (file)
@@ -77,6 +77,7 @@ import org.sonar.db.user.GroupMembershipDao;
 import org.sonar.db.user.RoleDao;
 import org.sonar.db.user.UserDao;
 import org.sonar.db.user.UserGroupDao;
+import org.sonar.db.user.UserPropertiesDao;
 import org.sonar.db.user.UserTokenDao;
 import org.sonar.db.webhook.WebhookDao;
 import org.sonar.db.webhook.WebhookDeliveryDao;
@@ -103,6 +104,7 @@ public class DbClient {
   private final UserDao userDao;
   private final UserGroupDao userGroupDao;
   private final UserTokenDao userTokenDao;
+  private final UserPropertiesDao userPropertiesDao;
   private final GroupMembershipDao groupMembershipDao;
   private final RoleDao roleDao;
   private final GroupPermissionDao groupPermissionDao;
@@ -172,6 +174,7 @@ public class DbClient {
     userDao = getDao(map, UserDao.class);
     userGroupDao = getDao(map, UserGroupDao.class);
     userTokenDao = getDao(map, UserTokenDao.class);
+    userPropertiesDao = getDao(map, UserPropertiesDao.class);
     groupMembershipDao = getDao(map, GroupMembershipDao.class);
     roleDao = getDao(map, RoleDao.class);
     groupPermissionDao = getDao(map, GroupPermissionDao.class);
@@ -301,6 +304,10 @@ public class DbClient {
     return userTokenDao;
   }
 
+  public UserPropertiesDao userPropertiesDao() {
+    return userPropertiesDao;
+  }
+
   public GroupMembershipDao groupMembershipDao() {
     return groupMembershipDao;
   }
index 931268827aff3d8b12351e68400636bea34123cb..1172e62b86da1666c5cb89a09e9ed914a76a6ea1 100644 (file)
@@ -131,6 +131,7 @@ import org.sonar.db.user.UserDto;
 import org.sonar.db.user.UserGroupDto;
 import org.sonar.db.user.UserGroupMapper;
 import org.sonar.db.user.UserMapper;
+import org.sonar.db.user.UserPropertiesMapper;
 import org.sonar.db.user.UserTokenCount;
 import org.sonar.db.user.UserTokenDto;
 import org.sonar.db.user.UserTokenMapper;
@@ -261,6 +262,7 @@ public class MyBatis implements Startable {
       UserGroupMapper.class,
       UserMapper.class,
       UserPermissionMapper.class,
+      UserPropertiesMapper.class,
       UserTokenMapper.class,
       WebhookMapper.class,
       WebhookDeliveryMapper.class
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesDao.java
new file mode 100644 (file)
index 0000000..835679c
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import java.util.List;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.Dao;
+import org.sonar.db.DbSession;
+
+public class UserPropertiesDao implements Dao {
+
+  private final System2 system2;
+  private final UuidFactory uuidFactory;
+
+  public UserPropertiesDao(System2 system2, UuidFactory uuidFactory) {
+    this.system2 = system2;
+    this.uuidFactory = uuidFactory;
+  }
+
+  public List<UserPropertyDto> selectByUser(DbSession session, UserDto user) {
+    return mapper(session).selectByUserUuid(user.getUuid());
+  }
+
+  public UserPropertyDto insertOrUpdate(DbSession session, UserPropertyDto dto) {
+    long now = system2.now();
+    if (mapper(session).update(dto, now) == 0) {
+      mapper(session).insert(dto.setUuid(uuidFactory.create()), now);
+    }
+    return dto;
+  }
+
+  public void deleteByUser(DbSession session, UserDto user) {
+    mapper(session).deleteByUserUuid(user.getUuid());
+  }
+
+  private static UserPropertiesMapper mapper(DbSession session) {
+    return session.getMapper(UserPropertiesMapper.class);
+  }
+
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertiesMapper.java
new file mode 100644 (file)
index 0000000..53a1ac7
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import java.util.List;
+import org.apache.ibatis.annotations.Param;
+
+public interface UserPropertiesMapper {
+
+  List<UserPropertyDto> selectByUserUuid(@Param("userUuid") String userUuid);
+
+  void insert(@Param("userProperty") UserPropertyDto userPropertyDto, @Param("now") long now);
+
+  int update(@Param("userProperty") UserPropertyDto userPropertyDto, @Param("now") long now);
+
+  void deleteByUserUuid(@Param("userUuid") String userUuid);
+
+}
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertyDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/user/UserPropertyDto.java
new file mode 100644 (file)
index 0000000..7ea88d0
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+public class UserPropertyDto {
+
+  /**
+   * Unique UUID identifier. Max size is 40. Can't be null.
+   */
+  private String uuid;
+
+  /**
+   * The UUID of the user the settings belongs to. Max size is 255. Can't be null.
+   */
+  private String userUuid;
+
+  /**
+   * The key of the settings. Max size is 100. Can't be null.
+   */
+  private String key;
+
+  /**
+   * The value of the settings. Max size is 4000. Can't be null.
+   */
+  private String value;
+
+  public String getUuid() {
+    return uuid;
+  }
+
+  UserPropertyDto setUuid(String uuid) {
+    this.uuid = uuid;
+    return this;
+  }
+
+  public String getUserUuid() {
+    return userUuid;
+  }
+
+  public UserPropertyDto setUserUuid(String userUuid) {
+    this.userUuid = userUuid;
+    return this;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public UserPropertyDto setKey(String key) {
+    this.key = key;
+    return this;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public UserPropertyDto setValue(String value) {
+    this.value = value;
+    return this;
+  }
+
+}
diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserPropertiesMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserPropertiesMapper.xml
new file mode 100644 (file)
index 0000000..28016eb
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "mybatis-3-mapper.dtd">
+
+<mapper namespace="org.sonar.db.user.UserPropertiesMapper">
+
+    <sql id="userPropertiesColumns">
+        us.uuid as uuid,
+        us.user_uuid as userUuid,
+        us.kee as "key",
+        us.text_value as "value"
+    </sql>
+
+    <select id="selectByUserUuid" parameterType="String" resultType="org.sonar.db.user.UserPropertyDto">
+        SELECT
+        <include refid="userPropertiesColumns"/>
+        FROM user_properties us
+        WHERE us.user_uuid=#{userUuid}
+    </select>
+
+    <insert id="insert" parameterType="map" useGeneratedKeys="false">
+        INSERT INTO user_properties (
+           uuid,
+           user_uuid,
+           kee,
+           text_value,
+           created_at,
+           updated_at
+        ) VALUES (
+           #{userProperty.uuid,jdbcType=VARCHAR},
+           #{userProperty.userUuid,jdbcType=VARCHAR},
+           #{userProperty.key,jdbcType=VARCHAR},
+           #{userProperty.value,jdbcType=VARCHAR},
+           #{now,jdbcType=BIGINT},
+           #{now,jdbcType=BIGINT}
+        )
+    </insert>
+
+    <update id="update" parameterType="map">
+        UPDATE user_properties SET
+           text_value = #{userProperty.value, jdbcType=VARCHAR},
+           updated_at = #{now,jdbcType=BIGINT}
+        WHERE
+           user_uuid = #{userProperty.userUuid, jdbcType=VARCHAR}
+           AND kee = #{userProperty.key, jdbcType=VARCHAR}
+    </update>
+
+    <update id="deleteByUserUuid" parameterType="String">
+        DELETE FROM user_properties WHERE user_uuid=#{userUuid,jdbcType=VARCHAR}
+    </update>
+
+</mapper>
index f03bfc80e0b320744324ddf427fc5125a913827f..2b32e081041418578e5826077975dde2d4ab710e 100644 (file)
@@ -30,6 +30,6 @@ public class DaoModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new DaoModule().configure(container);
-    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 59);
+    assertThat(container.size()).isEqualTo(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER + 60);
   }
 }
index 761b56264277bffd2e57e8e205efc69887418c9e..d7baf7809e2b11591130fd7f904208fc0c3f68b3 100644 (file)
@@ -23,12 +23,11 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.user.UserQuery;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.System2;
+import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.db.DatabaseUtils;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
@@ -42,15 +41,13 @@ import static java.util.Collections.emptyList;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.groups.Tuple.tuple;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 import static org.sonar.db.user.GroupTesting.newGroupDto;
 import static org.sonar.db.user.UserTesting.newUserDto;
 
 public class UserDaoTest {
   private static final long NOW = 1_500_000_000_000L;
 
-  private System2 system2 = mock(System2.class);
+  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
 
   @Rule
   public DbTester db = DbTester.create(system2);
@@ -59,11 +56,6 @@ public class UserDaoTest {
   private DbSession session = db.getSession();
   private UserDao underTest = db.getDbClient().userDao();
 
-  @Before
-  public void setUp() {
-    when(system2.now()).thenReturn(NOW);
-  }
-
   @Test
   public void selectByUuid() {
     UserDto user1 = db.users().insertUser();
@@ -363,11 +355,11 @@ public class UserDaoTest {
   public void countNewUsersSince() {
     assertThat(underTest.countNewUsersSince(session, 400L)).isEqualTo(0);
 
-    when(system2.now()).thenReturn(100L);
+    system2.setNow(100L);
     insertNonRootUser(newUserDto());
-    when(system2.now()).thenReturn(200L);
+    system2.setNow(200L);
     insertNonRootUser(newUserDto());
-    when(system2.now()).thenReturn(300L);
+    system2.setNow(300L);
     insertNonRootUser(newUserDto());
 
     assertThat(underTest.countNewUsersSince(session, 50L)).isEqualTo(3);
@@ -707,25 +699,25 @@ public class UserDaoTest {
     assertThat(underTest.selectByLogin(session, otherUser.getLogin()).isRoot()).isEqualTo(false);
 
     // does not fail when changing to same value
-    when(system2.now()).thenReturn(15_000L);
+    system2.setNow(15_000L);
     commit(() -> underTest.setRoot(session, login, false));
     verifyRootAndUpdatedAt(login, false, 15_000L);
     verifyRootAndUpdatedAt(otherUser.getLogin(), false, otherUser.getUpdatedAt());
 
     // change value
-    when(system2.now()).thenReturn(26_000L);
+    system2.setNow(26_000L);
     commit(() -> underTest.setRoot(session, login, true));
     verifyRootAndUpdatedAt(login, true, 26_000L);
     verifyRootAndUpdatedAt(otherUser.getLogin(), false, otherUser.getUpdatedAt());
 
     // does not fail when changing to same value
-    when(system2.now()).thenReturn(37_000L);
+    system2.setNow(37_000L);
     commit(() -> underTest.setRoot(session, login, true));
     verifyRootAndUpdatedAt(login, true, 37_000L);
     verifyRootAndUpdatedAt(otherUser.getLogin(), false, otherUser.getUpdatedAt());
 
     // change value back
-    when(system2.now()).thenReturn(48_000L);
+    system2.setNow(48_000L);
     commit(() -> underTest.setRoot(session, login, false));
     verifyRootAndUpdatedAt(login, false, 48_000L);
     verifyRootAndUpdatedAt(otherUser.getLogin(), false, otherUser.getUpdatedAt());
index bc9c2d793781d7794775c0bc5e791455e6ae4d68..516695d11efd5c8b01a6cedbd024d33cee4b6be8 100644 (file)
@@ -44,6 +44,7 @@ import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
 import static org.sonar.db.user.GroupTesting.newGroupDto;
 import static org.sonar.db.user.UserTesting.newDisabledUser;
 import static org.sonar.db.user.UserTesting.newUserDto;
+import static org.sonar.db.user.UserTesting.newUserSettingDto;
 import static org.sonar.db.user.UserTokenTesting.newUserToken;
 
 public class UserDbTester {
@@ -110,6 +111,17 @@ public class UserDbTester {
     return Optional.ofNullable(dbClient.userDao().selectByLogin(db.getSession(), login));
   }
 
+  // USER SETTINGS
+
+  @SafeVarargs
+  public final UserPropertyDto insertUserSetting(UserDto user, Consumer<UserPropertyDto>... populators) {
+    UserPropertyDto dto = newUserSettingDto(user);
+    stream(populators).forEach(p -> p.accept(dto));
+    dbClient.userPropertiesDao().insertOrUpdate(db.getSession(), dto);
+    db.commit();
+    return dto;
+  }
+
   // GROUPS
 
   public GroupDto insertGroup(OrganizationDto organization, String name) {
diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/user/UserPropertiesDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/user/UserPropertiesDaoTest.java
new file mode 100644 (file)
index 0000000..1f19360
--- /dev/null
@@ -0,0 +1,127 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.user;
+
+import java.util.List;
+import java.util.Map;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.internal.TestSystem2;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+
+public class UserPropertiesDaoTest {
+
+  private static final long NOW = 1_500_000_000_000L;
+
+  private TestSystem2 system2 = new TestSystem2().setNow(NOW);
+
+  @Rule
+  public DbTester db = DbTester.create(system2);
+
+  private UserPropertiesDao underTest = db.getDbClient().userPropertiesDao();
+
+  @Test
+  public void select_by_user() {
+    UserDto user = db.users().insertUser();
+    UserPropertyDto userSetting1 = db.users().insertUserSetting(user);
+    UserPropertyDto userSetting2 = db.users().insertUserSetting(user);
+    UserDto anotherUser = db.users().insertUser();
+    UserPropertyDto userSetting3 = db.users().insertUserSetting(anotherUser);
+
+    List<UserPropertyDto> results = underTest.selectByUser(db.getSession(), user);
+
+    assertThat(results)
+      .extracting(UserPropertyDto::getUuid)
+      .containsExactlyInAnyOrder(userSetting1.getUuid(), userSetting2.getUuid())
+      .doesNotContain(userSetting3.getUuid());
+  }
+
+  @Test
+  public void insert() {
+    UserDto user = db.users().insertUser();
+
+    UserPropertyDto userSetting = underTest.insertOrUpdate(db.getSession(), new UserPropertyDto()
+      .setUserUuid(user.getUuid())
+      .setKey("a_key")
+      .setValue("a_value"));
+
+    Map<String, Object> map = db.selectFirst(db.getSession(), "select uuid as \"uuid\",\n" +
+      " user_uuid as \"userUuid\",\n" +
+      " kee as \"key\",\n" +
+      " text_value as \"value\"," +
+      " created_at as \"createdAt\",\n" +
+      " updated_at as \"updatedAt\"" +
+      " from user_properties");
+    assertThat(map).contains(
+      entry("uuid", userSetting.getUuid()),
+      entry("userUuid", user.getUuid()),
+      entry("key", "a_key"),
+      entry("value", "a_value"),
+      entry("createdAt", NOW),
+      entry("updatedAt", NOW));
+  }
+
+  @Test
+  public void update() {
+    UserDto user = db.users().insertUser();
+    UserPropertyDto userProperty = underTest.insertOrUpdate(db.getSession(), new UserPropertyDto()
+      .setUserUuid(user.getUuid())
+      .setKey("a_key")
+      .setValue("old_value"));
+
+    system2.setNow(2_000_000_000_000L);
+    underTest.insertOrUpdate(db.getSession(), new UserPropertyDto()
+      .setUserUuid(user.getUuid())
+      .setKey("a_key")
+      .setValue("new_value"));
+
+    Map<String, Object> map = db.selectFirst(db.getSession(), "select uuid as \"uuid\",\n" +
+      " user_uuid as \"userUuid\",\n" +
+      " kee as \"key\",\n" +
+      " text_value as \"value\"," +
+      " created_at as \"createdAt\",\n" +
+      " updated_at as \"updatedAt\"" +
+      " from user_properties");
+    assertThat(map).contains(
+      entry("uuid", userProperty.getUuid()),
+      entry("userUuid", user.getUuid()),
+      entry("key", "a_key"),
+      entry("value", "new_value"),
+      entry("createdAt", NOW),
+      entry("updatedAt", 2_000_000_000_000L));
+  }
+
+  @Test
+  public void delete_by_user() {
+    UserDto user = db.users().insertUser();
+    db.users().insertUserSetting(user);
+    db.users().insertUserSetting(user);
+    UserDto anotherUser = db.users().insertUser();
+    db.users().insertUserSetting(anotherUser);
+
+    underTest.deleteByUser(db.getSession(), user);
+
+    assertThat(underTest.selectByUser(db.getSession(), user)).isEmpty();
+    assertThat(underTest.selectByUser(db.getSession(), anotherUser)).hasSize(1);
+  }
+}
index 8be30e35c60ff8862beb64a6e7348e0165d4f8b9..50e39364a6b1a5d71dfe0955954e5404445e9360 100644 (file)
@@ -20,6 +20,7 @@
 package org.sonar.db.user;
 
 import javax.annotation.Nullable;
+import org.sonar.core.util.Uuids;
 
 import static java.util.Collections.singletonList;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
@@ -87,4 +88,12 @@ public class UserTesting {
       .setCryptedPassword(null)
       .setSalt(null);
   }
+
+  public static UserPropertyDto newUserSettingDto(UserDto user) {
+    return new UserPropertyDto()
+      .setUuid(Uuids.createFast())
+      .setUserUuid(user.getUuid())
+      .setKey(randomAlphanumeric(20))
+      .setValue(randomAlphanumeric(100));
+  }
 }
index a196b02f2bc21517efaef5d1ce5ecdf82fb03766..4a6c471f6bd6f565207a3560c04d7c8c2373cfab 100644 (file)
@@ -39,6 +39,7 @@ import org.sonar.server.platform.db.migration.version.v72.DbVersion72;
 import org.sonar.server.platform.db.migration.version.v73.DbVersion73;
 import org.sonar.server.platform.db.migration.version.v74.DbVersion74;
 import org.sonar.server.platform.db.migration.version.v75.DbVersion75;
+import org.sonar.server.platform.db.migration.version.v76.DbVersion76;
 
 public class MigrationConfigurationModule extends Module {
   @Override
@@ -61,6 +62,7 @@ public class MigrationConfigurationModule extends Module {
       DbVersion73.class,
       DbVersion74.class,
       DbVersion75.class,
+      DbVersion76.class,
 
       // migration steps
       MigrationStepRegistryImpl.class,
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTable.java
new file mode 100644 (file)
index 0000000..1fd4df1
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+@SupportsBlueGreen
+public class AddUniqueIndexInUserPropertiesTable extends DdlChange {
+
+  private static final String TABLE_NAME = "user_properties";
+
+  public AddUniqueIndexInUserPropertiesTable(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    context.execute(new CreateIndexBuilder(getDialect())
+      .addColumn(newVarcharColumnDefBuilder()
+        .setColumnName("user_uuid")
+        .setIsNullable(false)
+        .setLimit(255)
+        .build())
+      .addColumn(newVarcharColumnDefBuilder()
+        .setColumnName("kee")
+        .setIsNullable(false)
+        .setLimit(100)
+        .build())
+      .setUnique(true)
+      .setTable(TABLE_NAME)
+      .setName("user_properties_user_uuid_kee")
+      .build());
+  }
+
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTable.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTable.java
new file mode 100644 (file)
index 0000000..59942a7
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.DatabaseUtils;
+import org.sonar.server.platform.db.migration.SupportsBlueGreen;
+import org.sonar.server.platform.db.migration.def.BigIntegerColumnDef;
+import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
+import org.sonar.server.platform.db.migration.sql.CreateTableBuilder;
+import org.sonar.server.platform.db.migration.step.DdlChange;
+
+import static org.sonar.server.platform.db.migration.def.BigIntegerColumnDef.newBigIntegerColumnDefBuilder;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.UUID_SIZE;
+import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;
+
+@SupportsBlueGreen
+public class CreateUserPropertiesTable extends DdlChange {
+
+  private static final String TABLE_NAME = "user_properties";
+
+  private static final VarcharColumnDef UUID_COLUMN = newVarcharColumnDefBuilder()
+    .setColumnName("uuid")
+    .setIsNullable(false)
+    .setLimit(UUID_SIZE)
+    .build();
+  private static final VarcharColumnDef USER_UUID_COLUMN = newVarcharColumnDefBuilder()
+    .setColumnName("user_uuid")
+    .setIsNullable(false)
+    .setLimit(255)
+    .build();
+  private static final VarcharColumnDef KEY_COLUMN = newVarcharColumnDefBuilder()
+    .setColumnName("kee")
+    .setIsNullable(false)
+    .setLimit(100)
+    .build();
+  private static final VarcharColumnDef TEXT_VALUE_COLUMN = newVarcharColumnDefBuilder()
+    .setColumnName("text_value")
+    .setIsNullable(false)
+    .setLimit(4000)
+    .build();
+  private static final BigIntegerColumnDef CREATED_AT_COLUMN = newBigIntegerColumnDefBuilder()
+    .setColumnName("created_at")
+    .setIsNullable(false)
+    .build();
+  private static final BigIntegerColumnDef UPDATED_AT_COLUMN = newBigIntegerColumnDefBuilder()
+    .setColumnName("updated_at")
+    .setIsNullable(false)
+    .build();
+
+  public CreateUserPropertiesTable(Database db) {
+    super(db);
+  }
+
+  @Override
+  public void execute(Context context) throws SQLException {
+    if (tableExists()) {
+      return;
+    }
+    context.execute(new CreateTableBuilder(getDialect(), TABLE_NAME)
+      .addPkColumn(UUID_COLUMN)
+      .addColumn(USER_UUID_COLUMN)
+      .addColumn(KEY_COLUMN)
+      .addColumn(TEXT_VALUE_COLUMN)
+      .addColumn(CREATED_AT_COLUMN)
+      .addColumn(UPDATED_AT_COLUMN)
+      .build());
+  }
+
+  private boolean tableExists() throws SQLException {
+    try (Connection connection = getDatabase().getDataSource().getConnection()) {
+      return DatabaseUtils.tableExists(TABLE_NAME, connection);
+    }
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76.java
new file mode 100644 (file)
index 0000000..41364b7
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import org.sonar.server.platform.db.migration.step.MigrationStepRegistry;
+import org.sonar.server.platform.db.migration.version.DbVersion;
+
+public class DbVersion76 implements DbVersion {
+
+  @Override
+  public void addSteps(MigrationStepRegistry registry) {
+    registry
+      .add(2500, "Create table USER_PROPERTIES", CreateUserPropertiesTable.class)
+      .add(2501, "Add index in table USER_PROPERTIES", AddUniqueIndexInUserPropertiesTable.class);
+  }
+}
diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/package-info.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v76/package-info.java
new file mode 100644 (file)
index 0000000..8cad354
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.server.platform.db.migration.version.v76;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
index 12e1334429059eceaad5dd4463d4aaca044ec20f..4c987cbb1ecfdd83905e3934c4f22cd0f9880b9c 100644 (file)
@@ -37,7 +37,7 @@ public class MigrationConfigurationModuleTest {
     assertThat(container.getPicoContainer().getComponentAdapters())
       .hasSize(COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER
         // DbVersion classes
-        + 16
+        + 17
         // Others
         + 3);
   }
index 7cac352e8f69cb1de71acd33ca62048579cf3afc..23c52fb4443c8910ac8324cb0ddd378a5a418237 100644 (file)
@@ -63,5 +63,4 @@ public class CreateOrganizationsAlmBindingsTableTest {
     db.assertUniqueIndex(TABLE, "org_alm_bindings_org", "organization_uuid");
     db.assertUniqueIndex(TABLE, "org_alm_bindings_install", "alm_app_install_uuid");
   }
-
 }
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest.java
new file mode 100644 (file)
index 0000000..6978ae7
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.db.CoreDbTester;
+
+public class AddUniqueIndexInUserPropertiesTableTest {
+
+  private static final String TABLE = "user_properties";
+
+  @Rule
+  public final CoreDbTester db = CoreDbTester.createForSchema(AddUniqueIndexInUserPropertiesTableTest.class, "user_properties.sql");
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private AddUniqueIndexInUserPropertiesTable underTest = new AddUniqueIndexInUserPropertiesTable(db.database());
+
+  @Test
+  public void creates_index() throws SQLException {
+    underTest.execute();
+
+    db.assertUniqueIndex(TABLE, "user_properties_user_uuid_kee", "user_uuid", "kee");
+  }
+
+  @Test
+  public void migration_is_not_re_entrant() throws SQLException {
+    underTest.execute();
+
+    expectedException.expect(IllegalStateException.class);
+
+    underTest.execute();
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTableTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/CreateUserPropertiesTableTest.java
new file mode 100644 (file)
index 0000000..16151e8
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import java.sql.SQLException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.db.CoreDbTester;
+
+import static java.sql.Types.BIGINT;
+import static java.sql.Types.VARCHAR;
+
+public class CreateUserPropertiesTableTest {
+
+  private static final String TABLE = "user_properties";
+
+  @Rule
+  public final CoreDbTester db = CoreDbTester.createEmpty();
+
+  private CreateUserPropertiesTable underTest = new CreateUserPropertiesTable(db.database());
+
+  @Test
+  public void creates_table() throws SQLException {
+    underTest.execute();
+
+    checkTable();
+  }
+
+  @Test
+  public void migration_is_reentrant() throws SQLException {
+    underTest.execute();
+    underTest.execute();
+
+    checkTable();
+  }
+
+  private void checkTable() {
+    db.assertPrimaryKey(TABLE, "pk_user_properties", "uuid");
+    db.assertColumnDefinition(TABLE, "uuid", VARCHAR, 40, false);
+    db.assertColumnDefinition(TABLE, "user_uuid", VARCHAR, 255, false);
+    db.assertColumnDefinition(TABLE, "kee", VARCHAR, 100, false);
+    db.assertColumnDefinition(TABLE, "text_value", VARCHAR, 4000, false);
+    db.assertColumnDefinition(TABLE, "created_at", BIGINT, null, false);
+    db.assertColumnDefinition(TABLE, "updated_at", BIGINT, null, false);
+  }
+}
diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v76/DbVersion76Test.java
new file mode 100644 (file)
index 0000000..2f71a6f
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.platform.db.migration.version.v76;
+
+import org.junit.Test;
+
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMigrationCount;
+import static org.sonar.server.platform.db.migration.version.DbVersionTestUtils.verifyMinimumMigrationNumber;
+
+public class DbVersion76Test {
+
+  private DbVersion76 underTest = new DbVersion76();
+
+  @Test
+  public void migrationNumber_starts_at_2500() {
+    verifyMinimumMigrationNumber(underTest, 2500);
+  }
+
+  @Test
+  public void verify_migration_count() {
+    verifyMigrationCount(underTest, 2);
+  }
+
+}
diff --git a/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest/user_properties.sql b/server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v76/AddUniqueIndexInUserPropertiesTableTest/user_properties.sql
new file mode 100644 (file)
index 0000000..11e714d
--- /dev/null
@@ -0,0 +1,10 @@
+CREATE TABLE "USER_PROPERTIES" (
+  "UUID" VARCHAR(40) NOT NULL,
+  "USER_UUID" VARCHAR(255) NOT NULL,
+  "KEE" VARCHAR(100) NOT NULL,
+  "TEXT_VALUE" VARCHAR(4000) NOT NULL,
+  "CREATED_AT" BIGINT NOT NULL,
+  "UPDATED_AT" BIGINT NOT NULL,
+
+  CONSTRAINT "PK_USER_PROPERTIES" PRIMARY KEY ("UUID")
+);
\ No newline at end of file
index 137fbcc95df18f1818d05e728adde2e363f746c8..0ab705264516d89d02a74725a79cf2baef4ad49d 100644 (file)
@@ -27,6 +27,7 @@ import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService.NewController;
 import org.sonar.core.platform.PluginRepository;
+import org.sonar.core.util.stream.MoreCollectors;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.ComponentDto;
@@ -125,7 +126,8 @@ public class CurrentAction implements UsersWsAction {
       .addAllScmAccounts(user.getScmAccountsAsList())
       .setPermissions(Permissions.newBuilder().addAllGlobal(getGlobalPermissions()).build())
       .setHomepage(buildHomepage(dbSession, user))
-      .setShowOnboardingTutorial(!user.isOnboarded());
+      .setShowOnboardingTutorial(!user.isOnboarded())
+      .addAllSettings(loadUserSettings(dbSession, user));
     setNullable(emptyToNull(user.getEmail()), builder::setEmail);
     setNullable(emptyToNull(user.getEmail()), u -> builder.setAvatar(avatarResolver.create(user)));
     setNullable(user.getExternalLogin(), builder::setExternalIdentity);
@@ -237,6 +239,16 @@ public class CurrentAction implements UsersWsAction {
       .build();
   }
 
+  private List<CurrentWsResponse.Setting> loadUserSettings(DbSession dbSession, UserDto user) {
+    return dbClient.userPropertiesDao().selectByUser(dbSession, user)
+      .stream()
+      .map(dto -> CurrentWsResponse.Setting.newBuilder()
+        .setKey(dto.getKey())
+        .setValue(dto.getValue())
+        .build())
+      .collect(MoreCollectors.toList());
+  }
+
   private static boolean noHomepageSet(UserDto user) {
     return user.getHomepageType() == null;
   }
index e177bc1f86c88246cbbc79d01f2528cefb505a39..96be02647800b139d5e2c2107abb5173771f00cd 100644 (file)
@@ -102,6 +102,7 @@ public class DeactivateAction implements UsersWsAction {
       dbClient.permissionTemplateDao().deleteUserPermissionsByUserId(dbSession, userId);
       dbClient.qProfileEditUsersDao().deleteByUser(dbSession, user);
       dbClient.organizationMemberDao().deleteByUserId(dbSession, userId);
+      dbClient.userPropertiesDao().deleteByUser(dbSession, user);
       dbClient.userDao().deactivateUser(dbSession, user);
       userIndexer.commitAndIndex(dbSession, user);
     }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java b/server/sonar-server/src/main/java/org/sonar/server/user/ws/SetSettingAction.java
new file mode 100644 (file)
index 0000000..97d1a3d
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.user.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.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.user.UserPropertyDto;
+import org.sonar.server.user.UserSession;
+
+import static java.util.Objects.requireNonNull;
+
+public class SetSettingAction implements UsersWsAction {
+
+  public static final String PARAM_KEY = "key";
+  public static final String PARAM_VALUE = "value";
+
+  private final DbClient dbClient;
+  private final UserSession userSession;
+
+  public SetSettingAction(DbClient dbClient, UserSession userSession) {
+    this.dbClient = dbClient;
+    this.userSession = userSession;
+  }
+
+  @Override
+  public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction("set_setting")
+      .setDescription("Update a setting value.<br>" +
+        "Requires user to be authenticated")
+      .setSince("7.6")
+      .setInternal(true)
+      .setPost(true)
+      .setHandler(this);
+
+    action.createParam(PARAM_KEY)
+      .setRequired(true)
+      .setMaximumLength(100)
+      .setDescription("Setting key")
+      .setPossibleValues("notifications.optOut", "notifications.readDate");
+
+    action.createParam(PARAM_VALUE)
+      .setRequired(true)
+      .setMaximumLength(4000)
+      .setDescription("Setting value")
+      .setExampleValue("true");
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    userSession.checkLoggedIn();
+    String key = request.mandatoryParam(PARAM_KEY);
+    String value = request.mandatoryParam(PARAM_VALUE);
+    setUserSetting(key, value);
+    response.noContent();
+  }
+
+  private void setUserSetting(String key, String value) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      dbClient.userPropertiesDao().insertOrUpdate(dbSession,
+        new UserPropertyDto()
+          .setUserUuid(requireNonNull(userSession.getUuid(), "Authenticated user uuid cannot be null"))
+          .setKey(key)
+          .setValue(value));
+      dbSession.commit();
+    }
+  }
+
+}
index 39d6c3ae5de512142b3cf8f808a3ced9d09e81bb..9ae8a68528786fa13089bf456aa2ce57f8d112e9 100644 (file)
@@ -38,6 +38,7 @@ public class UsersWsModule extends Module {
       UserJsonWriter.class,
       SkipOnboardingTutorialAction.class,
       SetHomepageAction.class,
-      HomepageTypesImpl.class);
+      HomepageTypesImpl.class,
+      SetSettingAction.class);
   }
 }
index a07acb7b511e056973e336b48037acd12a5a1e7e..1f9c32082b68771e30e50634f5bb3e772d397292 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.user.ws;
 
+import java.util.Collections;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -45,6 +46,7 @@ import org.sonarqube.ws.Users.CurrentWsResponse;
 
 import static com.google.common.collect.Lists.newArrayList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.Mockito.mock;
 import static org.sonar.api.web.UserRole.USER;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
@@ -56,7 +58,7 @@ import static org.sonar.test.JsonAssert.assertJson;
 
 public class CurrentActionTest {
   @Rule
-  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+  public UserSessionRule userSession = UserSessionRule.standalone();
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
   @Rule
@@ -70,7 +72,7 @@ public class CurrentActionTest {
     ResourceTypeTree.builder().addType(ResourceType.builder(Qualifiers.PROJECT).build()).build()}));
 
   private WsActionTester ws = new WsActionTester(
-    new CurrentAction(userSessionRule, db.getDbClient(), TestDefaultOrganizationProvider.from(db), new AvatarResolverImpl(), homepageTypes, pluginRepository, permissionService));
+    new CurrentAction(userSession, db.getDbClient(), TestDefaultOrganizationProvider.from(db), new AvatarResolverImpl(), homepageTypes, pluginRepository, permissionService));
 
   @Test
   public void return_user_info() {
@@ -83,7 +85,7 @@ public class CurrentActionTest {
       .setExternalIdentityProvider("sonarqube")
       .setScmAccounts(newArrayList("obiwan:github", "obiwan:bitbucket"))
       .setOnboarded(false));
-    userSessionRule.logIn("obiwan.kenobi");
+    userSession.logIn(user);
 
     CurrentWsResponse response = call();
 
@@ -97,7 +99,7 @@ public class CurrentActionTest {
 
   @Test
   public void return_minimal_user_info() {
-    db.users().insertUser(user -> user
+    UserDto user = db.users().insertUser(u -> u
       .setLogin("obiwan.kenobi")
       .setName("Obiwan Kenobi")
       .setEmail(null)
@@ -105,14 +107,14 @@ public class CurrentActionTest {
       .setExternalLogin("obiwan")
       .setExternalIdentityProvider("sonarqube")
       .setScmAccounts((String) null));
-    userSessionRule.logIn("obiwan.kenobi");
+    userSession.logIn(user);
 
     CurrentWsResponse response = call();
 
     assertThat(response)
       .extracting(CurrentWsResponse::getIsLoggedIn, CurrentWsResponse::getLogin, CurrentWsResponse::getName, CurrentWsResponse::hasAvatar, CurrentWsResponse::getLocal,
-        CurrentWsResponse::getExternalIdentity, CurrentWsResponse::getExternalProvider, CurrentWsResponse::hasPersonalOrganization)
-      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", false, true, "obiwan", "sonarqube", false);
+        CurrentWsResponse::getExternalIdentity, CurrentWsResponse::getExternalProvider, CurrentWsResponse::hasPersonalOrganization, CurrentWsResponse::getSettingsList)
+      .containsExactly(true, "obiwan.kenobi", "Obiwan Kenobi", false, true, "obiwan", "sonarqube", false, Collections.emptyList());
     assertThat(response.hasEmail()).isFalse();
     assertThat(response.getScmAccountsList()).isEmpty();
     assertThat(response.getGroupsList()).isEmpty();
@@ -121,10 +123,10 @@ public class CurrentActionTest {
 
   @Test
   public void convert_empty_email_to_null() {
-    db.users().insertUser(user -> user
+    UserDto user = db.users().insertUser(u -> u
       .setLogin("obiwan.kenobi")
       .setEmail(""));
-    userSessionRule.logIn("obiwan.kenobi");
+    userSession.logIn(user);
 
     CurrentWsResponse response = call();
 
@@ -134,7 +136,7 @@ public class CurrentActionTest {
   @Test
   public void return_group_membership() {
     UserDto user = db.users().insertUser();
-    userSessionRule.logIn(user.getLogin());
+    userSession.logIn(user);
     db.users().insertMember(db.users().insertGroup(newGroupDto().setName("Jedi")), user);
     db.users().insertMember(db.users().insertGroup(newGroupDto().setName("Rebel")), user);
 
@@ -146,8 +148,8 @@ public class CurrentActionTest {
   @Test
   public void return_permissions() {
     UserDto user = db.users().insertUser();
-    userSessionRule
-      .logIn(user.getLogin())
+    userSession
+      .logIn(user)
       // permissions on default organization
       .addPermission(SCAN, db.getDefaultOrganization())
       .addPermission(ADMINISTER_QUALITY_PROFILES, db.getDefaultOrganization())
@@ -163,17 +165,38 @@ public class CurrentActionTest {
   public void return_personal_organization() {
     OrganizationDto organization = db.organizations().insert();
     UserDto user = db.users().insertUser(u -> u.setOrganizationUuid(organization.getUuid()));
-    userSessionRule.logIn(user);
+    userSession.logIn(user);
 
     CurrentWsResponse response = call();
 
     assertThat(response.getPersonalOrganization()).isEqualTo(organization.getKey());
   }
 
+  @Test
+  public void return_user_settings() {
+    UserDto user = db.users().insertUser();
+    db.users().insertUserSetting(user, userSetting -> userSetting
+      .setKey("notifications.readDate")
+      .setValue("1234"));
+    db.users().insertUserSetting(user, userSetting -> userSetting
+      .setKey("notifications.optOut")
+      .setValue("true"));
+    db.commit();
+    userSession.logIn(user);
+
+    CurrentWsResponse response = call();
+
+    assertThat(response.getSettingsList())
+      .extracting(CurrentWsResponse.Setting::getKey, CurrentWsResponse.Setting::getValue)
+      .containsExactly(
+        tuple("notifications.optOut", "true"),
+        tuple("notifications.readDate", "1234"));
+  }
+
   @Test
   public void fail_with_ISE_when_user_login_in_db_does_not_exist() {
     db.users().insertUser(usert -> usert.setLogin("another"));
-    userSessionRule.logIn("obiwan.kenobi");
+    userSession.logIn("obiwan.kenobi");
 
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("User login 'obiwan.kenobi' cannot be found");
@@ -184,7 +207,7 @@ public class CurrentActionTest {
   @Test
   public void fail_with_ISE_when_personal_organization_does_not_exist() {
     UserDto user = db.users().insertUser(u -> u.setOrganizationUuid("Unknown"));
-    userSessionRule.logIn(user);
+    userSession.logIn(user);
 
     expectedException.expect(IllegalStateException.class);
     expectedException.expectMessage("Organization uuid 'Unknown' does not exist");
@@ -194,7 +217,7 @@ public class CurrentActionTest {
 
   @Test
   public void anonymous() {
-    userSessionRule
+    userSession
       .anonymous()
       .addPermission(SCAN, db.getDefaultOrganization())
       .addPermission(PROVISION_PROJECTS, db.getDefaultOrganization());
@@ -214,11 +237,6 @@ public class CurrentActionTest {
   @Test
   public void json_example() {
     ComponentDto componentDto = db.components().insertPrivateProject(u -> u.setUuid("UUID-of-the-death-star"), u -> u.setDbKey("death-star-key"));
-    userSessionRule
-      .logIn("obiwan.kenobi")
-      .addPermission(SCAN, db.getDefaultOrganization())
-      .addPermission(ADMINISTER_QUALITY_PROFILES, db.getDefaultOrganization())
-      .addProjectPermission(USER, componentDto);
     UserDto obiwan = db.users().insertUser(user -> user
       .setLogin("obiwan.kenobi")
       .setName("Obiwan Kenobi")
@@ -230,6 +248,11 @@ public class CurrentActionTest {
       .setOnboarded(true)
       .setHomepageType("PROJECT")
       .setHomepageParameter("UUID-of-the-death-star"));
+    userSession
+      .logIn(obiwan)
+      .addPermission(SCAN, db.getDefaultOrganization())
+      .addPermission(ADMINISTER_QUALITY_PROFILES, db.getDefaultOrganization())
+      .addProjectPermission(USER, componentDto);
     db.users().insertMember(db.users().insertGroup(newGroupDto().setName("Jedi")), obiwan);
     db.users().insertMember(db.users().insertGroup(newGroupDto().setName("Rebel")), obiwan);
 
index 04e4126c093d3a6a268b4c48d419e81485b33f1c..1e114fa0a34713f6219803d08a57659bcf936f22 100644 (file)
@@ -20,7 +20,6 @@
 package org.sonar.server.user.ws;
 
 import java.util.Optional;
-import java.util.function.Consumer;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -57,7 +56,6 @@ import static org.elasticsearch.index.query.QueryBuilders.boolQuery;
 import static org.elasticsearch.index.query.QueryBuilders.termQuery;
 import static org.sonar.api.web.UserRole.CODEVIEWER;
 import static org.sonar.api.web.UserRole.USER;
-import static org.sonar.core.permission.GlobalPermissions.SYSTEM_ADMIN;
 import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER;
 import static org.sonar.db.permission.OrganizationPermission.ADMINISTER_QUALITY_PROFILES;
@@ -93,14 +91,14 @@ public class DeactivateActionTest {
 
   @Test
   public void deactivate_user_and_delete_his_related_data() {
-    UserDto user = insertUser(u -> u
+    UserDto user = db.users().insertUser(u -> u
       .setLogin("ada.lovelace")
       .setEmail("ada.lovelace@noteg.com")
       .setName("Ada Lovelace")
       .setScmAccounts(singletonList("al")));
     logInAsSystemAdministrator();
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     verifyThatUserIsDeactivated(user.getLogin());
     assertThat(es.client().prepareSearch(UserIndexDefinition.INDEX_TYPE_USER)
@@ -113,12 +111,12 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_group_membership() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     GroupDto group1 = db.users().insertGroup();
     db.users().insertGroup();
     db.users().insertMember(group1, user);
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().groupMembershipDao().selectGroupIdsByUserId(dbSession, user.getId())).isEmpty();
   }
@@ -126,12 +124,12 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_tokens() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     db.users().insertToken(user);
     db.users().insertToken(user);
     db.commit();
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().userTokenDao().selectByUser(dbSession, user)).isEmpty();
   }
@@ -139,13 +137,13 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_properties() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     ComponentDto project = db.components().insertPrivateProject();
     db.properties().insertProperty(newUserPropertyDto(user));
     db.properties().insertProperty(newUserPropertyDto(user));
     db.properties().insertProperty(newUserPropertyDto(user).setResourceId(project.getId()));
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().propertiesDao().selectByQuery(PropertyQuery.builder().setUserId(user.getId()).build(), dbSession)).isEmpty();
     assertThat(db.getDbClient().propertiesDao().selectByQuery(PropertyQuery.builder().setUserId(user.getId()).setComponentId(project.getId()).build(), dbSession)).isEmpty();
@@ -154,14 +152,14 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_permissions() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     ComponentDto project = db.components().insertPrivateProject();
     db.users().insertPermissionOnUser(user, SCAN);
     db.users().insertPermissionOnUser(user, ADMINISTER_QUALITY_PROFILES);
     db.users().insertProjectPermissionOnUser(user, USER, project);
     db.users().insertProjectPermissionOnUser(user, CODEVIEWER, project);
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().userPermissionDao().selectGlobalPermissionsOfUser(dbSession, user.getId(), db.getDefaultOrganization().getUuid())).isEmpty();
     assertThat(db.getDbClient().userPermissionDao().selectProjectPermissionsOfUser(dbSession, user.getId(), project.getId())).isEmpty();
@@ -170,13 +168,13 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_permission_templates() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     PermissionTemplateDto template = db.permissionTemplates().insertTemplate();
     PermissionTemplateDto anotherTemplate = db.permissionTemplates().insertTemplate();
     db.permissionTemplates().addUserToTemplate(template.getId(), user.getId(), USER);
     db.permissionTemplates().addUserToTemplate(anotherTemplate.getId(), user.getId(), CODEVIEWER);
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().permissionTemplateDao().selectUserPermissionsByTemplateId(dbSession, template.getId())).extracting(PermissionTemplateUserDto::getUserId).isEmpty();
     assertThat(db.getDbClient().permissionTemplateDao().selectUserPermissionsByTemplateId(dbSession, anotherTemplate.getId())).extracting(PermissionTemplateUserDto::getUserId)
@@ -186,11 +184,11 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_qprofiles_permissions() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     QProfileDto profile = db.qualityProfiles().insert(db.getDefaultOrganization());
     db.qualityProfiles().addUserPermission(profile, user);
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().qProfileEditUsersDao().exists(dbSession, profile, user)).isFalse();
   }
@@ -198,14 +196,14 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_default_assignee_settings() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     ComponentDto project = db.components().insertPrivateProject();
     ComponentDto anotherProject = db.components().insertPrivateProject();
     db.properties().insertProperty(new PropertyDto().setKey("sonar.issues.defaultAssigneeLogin").setValue(user.getLogin()).setResourceId(project.getId()));
     db.properties().insertProperty(new PropertyDto().setKey("sonar.issues.defaultAssigneeLogin").setValue(user.getLogin()).setResourceId(anotherProject.getId()));
     db.properties().insertProperty(new PropertyDto().setKey("other").setValue(user.getLogin()).setResourceId(anotherProject.getId()));
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(db.getDbClient().propertiesDao().selectByQuery(PropertyQuery.builder().setKey("sonar.issues.defaultAssigneeLogin").build(), db.getSession())).isEmpty();
     assertThat(db.getDbClient().propertiesDao().selectByQuery(PropertyQuery.builder().build(), db.getSession())).extracting(PropertyDto::getKey).containsOnly("other");
@@ -214,21 +212,36 @@ public class DeactivateActionTest {
   @Test
   public void deactivate_user_deletes_his_organization_membership() {
     logInAsSystemAdministrator();
-    UserDto user = insertUser();
+    UserDto user = db.users().insertUser();
     OrganizationDto organization = db.organizations().insert();
     db.organizations().addMember(organization, user);
     OrganizationDto anotherOrganization = db.organizations().insert();
     db.organizations().addMember(anotherOrganization, user);
 
-    deactivate(user.getLogin()).getInput();
+    deactivate(user.getLogin());
 
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), organization.getUuid(), user.getId())).isNotPresent();
     assertThat(dbClient.organizationMemberDao().select(db.getSession(), anotherOrganization.getUuid(), user.getId())).isNotPresent();
   }
 
+  @Test
+  public void deactivate_user_deletes_his_user_settings() {
+    logInAsSystemAdministrator();
+    UserDto user = db.users().insertUser();
+    db.users().insertUserSetting(user);
+    db.users().insertUserSetting(user);
+    UserDto anotherUser = db.users().insertUser();
+    db.users().insertUserSetting(anotherUser);
+
+    deactivate(user.getLogin());
+
+    assertThat(db.getDbClient().userPropertiesDao().selectByUser(dbSession, user)).isEmpty();
+    assertThat(db.getDbClient().userPropertiesDao().selectByUser(dbSession, anotherUser)).hasSize(1);
+  }
+
   @Test
   public void cannot_deactivate_self() {
-    UserDto user = createUser();
+    UserDto user = db.users().insertUser();
     userSession.logIn(user.getLogin()).setSystemAdministrator();
 
     expectedException.expect(BadRequestException.class);
@@ -279,7 +292,7 @@ public class DeactivateActionTest {
 
   @Test
   public void fail_to_deactivate_last_administrator_of_default_organization() {
-    UserDto admin = createUser();
+    UserDto admin = db.users().insertUser();
     db.users().insertPermissionOnUser(admin, ADMINISTER);
     logInAsSystemAdministrator();
 
@@ -293,15 +306,15 @@ public class DeactivateActionTest {
   public void fail_to_deactivate_last_administrator_of_organization() {
     // user1 is the unique administrator of org1 and org2.
     // user1 and user2 are both administrators of org3
-    UserDto user1 = insertUser(u -> u.setLogin("test"));
+    UserDto user1 = db.users().insertUser(u -> u.setLogin("test"));
     OrganizationDto org1 = db.organizations().insert(newOrganizationDto().setKey("org1"));
     OrganizationDto org2 = db.organizations().insert(newOrganizationDto().setKey("org2"));
     OrganizationDto org3 = db.organizations().insert(newOrganizationDto().setKey("org3"));
-    db.users().insertPermissionOnUser(org1, user1, SYSTEM_ADMIN);
-    db.users().insertPermissionOnUser(org2, user1, SYSTEM_ADMIN);
-    db.users().insertPermissionOnUser(org3, user1, SYSTEM_ADMIN);
-    UserDto user2 = createUser();
-    db.users().insertPermissionOnUser(org3, user2, SYSTEM_ADMIN);
+    db.users().insertPermissionOnUser(org1, user1, ADMINISTER);
+    db.users().insertPermissionOnUser(org2, user1, ADMINISTER);
+    db.users().insertPermissionOnUser(org3, user1, ADMINISTER);
+    UserDto user2 = db.users().insertUser();
+    db.users().insertPermissionOnUser(org3, user2, ADMINISTER);
     logInAsSystemAdministrator();
 
     expectedException.expect(BadRequestException.class);
@@ -312,8 +325,8 @@ public class DeactivateActionTest {
 
   @Test
   public void administrators_can_be_deactivated_if_there_are_still_other_administrators() {
-    UserDto admin = createUser();
-    UserDto anotherAdmin = createUser();
+    UserDto admin = db.users().insertUser();
+    UserDto anotherAdmin = db.users().insertUser();
     db.users().insertPermissionOnUser(admin, ADMINISTER);
     db.users().insertPermissionOnUser(anotherAdmin, ADMINISTER);
     db.commit();
@@ -334,7 +347,7 @@ public class DeactivateActionTest {
 
   @Test
   public void test_example() {
-    UserDto user = insertUser(u -> u
+    UserDto user = db.users().insertUser(u -> u
       .setLogin("ada.lovelace")
       .setEmail("ada.lovelace@noteg.com")
       .setName("Ada Lovelace")
@@ -347,19 +360,6 @@ public class DeactivateActionTest {
     assertJson(json).isSimilarTo(ws.getDef().responseExampleAsString());
   }
 
-  private UserDto createUser() {
-    return insertUser();
-  }
-
-  @SafeVarargs
-  private final UserDto insertUser(Consumer<UserDto>... populators) {
-    UserDto user = db.users().insertUser(populators);
-    db.users().insertToken(user);
-    db.properties().insertProperties(new PropertyDto().setUserId(user.getId()).setKey("foo").setValue("bar"));
-    userIndexer.commitAndIndex(dbSession, user);
-    return user;
-  }
-
   private void logInAsSystemAdministrator() {
     userSession.logIn().setSystemAdministrator();
   }
diff --git a/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/user/ws/SetSettingActionTest.java
new file mode 100644 (file)
index 0000000..8b9f526
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+package org.sonar.server.user.ws;
+
+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.db.DbTester;
+import org.sonar.db.user.UserDto;
+import org.sonar.db.user.UserPropertyDto;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+public class SetSettingActionTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+  @Rule
+  public UserSessionRule userSession = UserSessionRule.standalone();
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+
+  private WsActionTester ws = new WsActionTester(new SetSettingAction(db.getDbClient(), userSession));
+
+  @Test
+  public void set_new_setting() {
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user);
+
+    ws.newRequest()
+      .setParam("key", "notifications.optOut")
+      .setParam("value", "true")
+      .execute();
+
+    assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
+      .extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
+      .containsExactlyInAnyOrder(tuple("notifications.optOut", "true"));
+  }
+
+  @Test
+  public void update_existing_setting() {
+    UserDto user = db.users().insertUser();
+    db.users().insertUserSetting(user, userSetting -> userSetting
+      .setKey("notifications.optOut")
+      .setValue("false"));
+    userSession.logIn(user);
+
+    ws.newRequest()
+      .setParam("key", "notifications.optOut")
+      .setParam("value", "true")
+      .execute();
+
+    assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
+      .extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
+      .containsExactlyInAnyOrder(tuple("notifications.optOut", "true"));
+  }
+
+  @Test
+  public void keep_existing_setting_when_setting_new_one() {
+    UserDto user = db.users().insertUser();
+    db.users().insertUserSetting(user, userSetting -> userSetting
+      .setKey("notifications.readDate")
+      .setValue("1234"));
+    userSession.logIn(user);
+
+    ws.newRequest()
+      .setParam("key", "notifications.optOut")
+      .setParam("value", "true")
+      .execute();
+
+    assertThat(db.getDbClient().userPropertiesDao().selectByUser(db.getSession(), user))
+      .extracting(UserPropertyDto::getKey, UserPropertyDto::getValue)
+      .containsExactlyInAnyOrder(
+        tuple("notifications.readDate", "1234"),
+        tuple("notifications.optOut", "true"));
+  }
+
+  @Test
+  public void fail_when_not_authenticated() {
+    expectedException.expect(UnauthorizedException.class);
+
+    ws.newRequest()
+      .setParam("key", "notifications.optOut")
+      .setParam("value", "true")
+      .execute();
+  }
+
+  @Test
+  public void definition() {
+    WebService.Action definition = ws.getDef();
+
+    assertThat(definition.key()).isEqualTo("set_setting");
+    assertThat(definition.isPost()).isTrue();
+    assertThat(definition.isInternal()).isTrue();
+    assertThat(definition.since()).isEqualTo("7.6");
+
+    assertThat(definition.params())
+      .extracting(WebService.Param::key, WebService.Param::isRequired, WebService.Param::maximumLength)
+      .containsOnly(
+        tuple("key", true, 100),
+        tuple("value", true, 4000));
+
+    assertThat(definition.param("key").possibleValues()).containsExactlyInAnyOrder("notifications.optOut", "notifications.readDate");
+  }
+
+}
index ad4e8668dd5d7744e723d7f8c572cebf9945af1c..e95268bd442538c25c282005bd96349b91c7ae82 100644 (file)
@@ -29,6 +29,6 @@ public class UsersWsModuleTest {
   public void verify_count_of_added_components() {
     ComponentContainer container = new ComponentContainer();
     new UsersWsModule().configure(container);
-    assertThat(container.size()).isEqualTo(2 + 14);
+    assertThat(container.size()).isEqualTo(2 + 15);
   }
 }
diff --git a/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetSettingRequest.java b/sonar-ws/src/main/java/org/sonarqube/ws/client/users/SetSettingRequest.java
new file mode 100644 (file)
index 0000000..f69d272
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonarqube.ws.client.users;
+
+import javax.annotation.Generated;
+
+/**
+ * This is part of the internal API.
+ * This is a POST request.
+ * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/set_setting">Further information about this action online (including a response example)</a>
+ *  @since 7.6
+ */
+@Generated("sonar-ws-generator")
+public class SetSettingRequest {
+
+  private String key;
+  private String value;
+
+  /**
+   * This is a mandatory parameter.
+   */
+  public SetSettingRequest setKey(String key) {
+    this.key = key;
+    return this;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  /**
+   * This is a mandatory parameter.
+   * Possible values:
+   * <ul>
+   *   <li>"notifications.optOut"</li>
+   *   <li>"notifications.readDate"</li>
+   * </ul>
+   */
+  public SetSettingRequest setValue(String value) {
+    this.value = value;
+    return this;
+  }
+
+  public String getValue() {
+    return value;
+  }
+}
index 3dcec5eaa393295fe2282cec82309a3408dd8a54..beb341d56e6d2c1c0e50e051e2727af7a279f2f7 100644 (file)
@@ -22,15 +22,15 @@ package org.sonarqube.ws.client.users;
 import java.util.stream.Collectors;
 import javax.annotation.Generated;
 import org.sonarqube.ws.MediaTypes;
-import org.sonarqube.ws.client.BaseService;
-import org.sonarqube.ws.client.GetRequest;
-import org.sonarqube.ws.client.PostRequest;
-import org.sonarqube.ws.client.WsConnector;
 import org.sonarqube.ws.Users.CreateWsResponse;
 import org.sonarqube.ws.Users.CurrentWsResponse;
 import org.sonarqube.ws.Users.GroupsWsResponse;
 import org.sonarqube.ws.Users.IdentityProvidersWsResponse;
 import org.sonarqube.ws.Users.SearchWsResponse;
+import org.sonarqube.ws.client.BaseService;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.PostRequest;
+import org.sonarqube.ws.client.WsConnector;
 
 /**
  * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users">Further information about this web service online</a>
@@ -172,6 +172,22 @@ public class UsersService extends BaseService {
       ).content();
   }
 
+  /**
+   *
+   * This is part of the internal API.
+   * This is a POST request.
+   * @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/users/set_setting">Further information about this action online (including a response example)</a>
+   * @since 7.6
+   */
+  public void setSetting(SetSettingRequest request) {
+    call(
+      new PostRequest(path("set_setting"))
+        .setParam("key", request.getKey())
+        .setParam("value", request.getValue())
+        .setMediaType(MediaTypes.JSON)
+    ).content();
+  }
+
   /**
    *
    * This is part of the internal API.
index d3ba178ebeab93c39413ae8bf2435dd6c737ae71..917cd7c2b586c090c088f53860ae819a0bfcb552 100644 (file)
@@ -110,6 +110,7 @@ message CurrentWsResponse {
   optional string avatar = 12;
   optional Homepage homepage = 13;
   optional string personalOrganization = 14;
+  repeated Setting settings = 15;
 
   message Permissions {
     repeated string global = 1;
@@ -133,4 +134,9 @@ message CurrentWsResponse {
     optional string organization = 3;
     optional string branch = 4;
   }
+
+  message Setting {
+    optional string key = 1;
+    optional string value = 2;
+  }
 }