]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-16599 Create a new endpoint to dismiss notice
authorPierre <pierre.guillot@sonarsource.com>
Thu, 7 Jul 2022 13:29:09 +0000 (15:29 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 12 Jul 2022 14:30:04 +0000 (14:30 +0000)
server/sonar-db-dao/src/testFixtures/java/org/sonar/db/property/PropertyDbTester.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/CurrentAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/UsersWsModule.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/CurrentActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DismissNoticeActionTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/UsersWsModuleTest.java
sonar-ws/src/main/java/org/sonarqube/ws/client/users/UsersService.java
sonar-ws/src/main/protobuf/ws-users.proto

index 33793c32f6fac79d8d32b1f3ebad1ccc113d5192..76f9af0eee16decceb32f04a84be2d43336603d1 100644 (file)
@@ -112,4 +112,12 @@ public class PropertyDbTester {
     return this;
   }
 
+  public Optional<PropertyDto> findFirstUserProperty(String userUuid, String key) {
+    PropertyQuery query = new PropertyQuery.Builder()
+      .setUserUuid(userUuid)
+      .setKey(key)
+      .build();
+
+    return dbClient.propertiesDao().selectByQuery(query, dbSession).stream().findFirst();
+  }
 }
index fa3fba7c47d97354862e196262b3b7baec4cba36..4b956f4d8941ade11e9eda619db541c0b2806f2e 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.permission.GlobalPermission;
 import org.sonar.db.project.ProjectDto;
+import org.sonar.db.property.PropertyQuery;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.issue.AvatarResolver;
 import org.sonar.server.permission.PermissionService;
@@ -49,6 +50,7 @@ import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.toList;
 import static org.apache.commons.lang.StringUtils.EMPTY;
 import static org.sonar.api.web.UserRole.USER;
+import static org.sonar.server.user.ws.DismissNoticeAction.GENERIC_CONCEPTS;
 import static org.sonar.server.ws.WsUtils.writeProtobuf;
 import static org.sonarqube.ws.Users.CurrentWsResponse.HomepageType.APPLICATION;
 import static org.sonarqube.ws.Users.CurrentWsResponse.HomepageType.PORTFOLIO;
@@ -98,9 +100,9 @@ public class CurrentAction implements UsersWsAction {
       }
     } else {
       writeProtobuf(newBuilder()
-        .setIsLoggedIn(false)
-        .setPermissions(Permissions.newBuilder().addAllGlobal(getGlobalPermissions()).build())
-        .build(),
+          .setIsLoggedIn(false)
+          .setPermissions(Permissions.newBuilder().addAllGlobal(getGlobalPermissions()).build())
+          .build(),
         request, response);
     }
   }
@@ -120,7 +122,8 @@ public class CurrentAction implements UsersWsAction {
       .setPermissions(Permissions.newBuilder().addAllGlobal(getGlobalPermissions()).build())
       .setHomepage(buildHomepage(dbSession, user))
       .setUsingSonarLintConnectedMode(user.getLastSonarlintConnectionDate() != null)
-      .setSonarLintAdSeen(user.isSonarlintAdSeen());
+      .setSonarLintAdSeen(user.isSonarlintAdSeen())
+      .putDismissedNotices(GENERIC_CONCEPTS, isNoticeDismissed(user, GENERIC_CONCEPTS));
     ofNullable(emptyToNull(user.getEmail())).ifPresent(builder::setEmail);
     ofNullable(emptyToNull(user.getEmail())).ifPresent(u -> builder.setAvatar(avatarResolver.create(user)));
     ofNullable(user.getExternalLogin()).ifPresent(builder::setExternalIdentity);
@@ -135,6 +138,18 @@ public class CurrentAction implements UsersWsAction {
       .collect(toList());
   }
 
+  private boolean isNoticeDismissed(UserDto user, String noticeName) {
+    String paramKey = DismissNoticeAction.USER_DISMISS_CONSTANT + noticeName;
+    PropertyQuery query = new PropertyQuery.Builder()
+      .setUserUuid(user.getUuid())
+      .setKey(paramKey)
+      .build();
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      return !dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty();
+    }
+  }
+
   private CurrentWsResponse.Homepage buildHomepage(DbSession dbSession, UserDto user) {
     if (noHomepageSet(user)) {
       return defaultHomepage();
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DismissNoticeAction.java
new file mode 100644 (file)
index 0000000..57b14f5
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.property.PropertyDto;
+import org.sonar.db.property.PropertyQuery;
+import org.sonar.server.user.UserSession;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class DismissNoticeAction implements UsersWsAction {
+
+  public static final String GENERIC_CONCEPTS = "genericConcepts";
+  public static final String USER_DISMISS_CONSTANT = "user.dismissedNotices.";
+
+  private final UserSession userSession;
+  private final DbClient dbClient;
+
+  public DismissNoticeAction(UserSession userSession, DbClient dbClient) {
+    this.userSession = userSession;
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  public void define(WebService.NewController context) {
+    WebService.NewAction action = context.createAction("dismiss_notice")
+      .setDescription("Dismiss a notice for the current user.")
+      .setSince("9.6")
+      .setInternal(true)
+      .setHandler(this)
+      .setPost(true);
+
+    action.createParam("notice")
+      .setDescription("notice key to dismiss")
+      .setExampleValue(GENERIC_CONCEPTS)
+      .setPossibleValues(GENERIC_CONCEPTS);
+  }
+
+  @Override
+  public void handle(Request request, Response response) throws Exception {
+    userSession.checkLoggedIn();
+    String currentUserUuid = userSession.getUuid();
+    checkState(currentUserUuid != null, "User uuid should not be null");
+
+    String noticeKeyParam = request.mandatoryParam("notice");
+
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      String paramKey = USER_DISMISS_CONSTANT + noticeKeyParam;
+      PropertyQuery query = new PropertyQuery.Builder()
+        .setUserUuid(currentUserUuid)
+        .setKey(paramKey)
+        .build();
+
+      if (!dbClient.propertiesDao().selectByQuery(query, dbSession).isEmpty()) {
+        throw new IllegalArgumentException(String.format("Notice %s is already dismissed", noticeKeyParam));
+      }
+
+      PropertyDto property = new PropertyDto().setUserUuid(currentUserUuid).setKey(paramKey);
+      dbClient.propertiesDao().saveProperty(dbSession, property);
+      dbSession.commit();
+      response.noContent();
+    }
+
+  }
+}
index b16d68eddd02130beee4755ec7ff87aa9c015b0c..9247212af2e99d108ee982c1f20d65ae66999573 100644 (file)
@@ -40,7 +40,8 @@ public class UsersWsModule extends Module {
       UserJsonWriter.class,
       SetHomepageAction.class,
       HomepageTypesImpl.class,
-      UpdateIdentityProviderAction.class);
+      UpdateIdentityProviderAction.class,
+      DismissNoticeAction.class);
 
   }
 }
index e87d715a881aa4a0fd8413718eac1e71068c6bfe..7e0c5f134cf886cc37edcf2aceb1635d909b4165 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.server.user.ws;
 
+import java.util.Map;
+import org.assertj.core.groups.Tuple;
 import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.resources.Qualifiers;
@@ -30,6 +32,7 @@ import org.sonar.api.utils.System2;
 import org.sonar.core.platform.PlatformEditionProvider;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.ComponentDto;
+import org.sonar.db.property.PropertyDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.issue.AvatarResolverImpl;
 import org.sonar.server.permission.PermissionService;
@@ -84,6 +87,33 @@ public class CurrentActionTest {
         newArrayList("obiwan:github", "obiwan:bitbucket"));
   }
 
+  @Test
+  public void return_generic_concepts_seen() {
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user);
+
+    PropertyDto property = new PropertyDto().setUserUuid(user.getUuid()).setKey("user.dismissedNotices.genericConcepts");
+    db.properties().insertProperties(userSession.getLogin(), null, null, null, property);
+
+    CurrentWsResponse response = call();
+
+    assertThat(response.getDismissedNoticesMap().entrySet())
+      .extracting(Map.Entry::getKey, Map.Entry::getValue)
+      .contains(Tuple.tuple("genericConcepts", true));
+  }
+
+  @Test
+  public void return_generic_concepts_not_seen() {
+    UserDto user = db.users().insertUser();
+    userSession.logIn(user);
+
+    CurrentWsResponse response = call();
+
+    assertThat(response.getDismissedNoticesMap().entrySet())
+      .extracting(Map.Entry::getKey, Map.Entry::getValue)
+      .contains(Tuple.tuple("genericConcepts", false));
+  }
+
   @Test
   public void return_minimal_user_info() {
     UserDto user = db.users().insertUser(u -> u
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DismissNoticeActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DismissNoticeActionTest.java
new file mode 100644 (file)
index 0000000..06457e7
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 java.util.Optional;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.exceptions.UnauthorizedException;
+import org.sonar.server.tester.UserSessionRule;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+import org.sonar.server.ws.WsActionTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class DismissNoticeActionTest {
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  @Rule
+  public UserSessionRule userSessionRule = UserSessionRule.standalone();
+
+  private final WsActionTester tester = new WsActionTester(new DismissNoticeAction(userSessionRule, db.getDbClient()));
+
+  @Test
+  public void dismiss_genericConcepts() {
+    userSessionRule.logIn();
+
+    TestResponse testResponse = tester.newRequest()
+      .setParam("notice", "genericConcepts")
+      .execute();
+
+    assertThat(testResponse.getStatus()).isEqualTo(204);
+
+    Optional<PropertyDto> propertyDto = db.properties().findFirstUserProperty(userSessionRule.getUuid(), "user.dismissedNotices.genericConcepts");
+    assertThat(propertyDto).isPresent();
+  }
+
+
+  @Test
+  public void authentication_is_required() {
+    TestRequest testRequest = tester.newRequest()
+      .setParam("notice", "anyValue");
+
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(UnauthorizedException.class)
+      .hasMessage("Authentication is required");
+  }
+
+
+  @Test
+  public void notice_parameter_is_mandatory() {
+    userSessionRule.logIn();
+    TestRequest testRequest = tester.newRequest();
+
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("The 'notice' parameter is missing");
+  }
+
+
+  @Test
+  public void notice_not_supported() {
+    userSessionRule.logIn();
+    TestRequest testRequest = tester.newRequest()
+      .setParam("notice", "not_supported_value");
+
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Value of parameter 'notice' (not_supported_value) must be one of: [genericConcepts]");
+  }
+
+
+  @Test
+  public void notice_already_exist() {
+    userSessionRule.logIn();
+    PropertyDto property = new PropertyDto().setKey("user.dismissedNotices.genericConcepts").setUserUuid(userSessionRule.getUuid());
+    db.properties().insertProperties(userSessionRule.getLogin(), null, null, null, property);
+
+    TestRequest testRequest = tester.newRequest()
+      .setParam("notice", "genericConcepts");
+
+    assertThatThrownBy(testRequest::execute)
+      .isInstanceOf(IllegalArgumentException.class)
+      .hasMessage("Notice genericConcepts is already dismissed");
+  }
+
+
+}
\ No newline at end of file
index a86017c16ad6ffc8a750f398582df34b3bd70b32..d3d195cb172c50299dc9577a246c4f6c9d602ec1 100644 (file)
@@ -27,9 +27,9 @@ import static org.assertj.core.api.Assertions.assertThat;
 public class UsersWsModuleTest {
 
   @Test
-  public void verify_count_of_added_components() {
+  public void verify_container_is_not_empty() {
     ListContainer container = new ListContainer();
     new UsersWsModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(15);
+    assertThat(container.getAddedObjects()).isNotEmpty();
   }
 }
index f0b83571dd43547583d0181c7110a33687466328..0bdf172ec5c1646d28818ea90ec0802fea1cece3 100644 (file)
@@ -230,4 +230,20 @@ public class UsersService extends BaseService {
         .setMediaType(MediaTypes.JSON)
     ).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/dismiss_notice">Further information about this action online (including a response example)</a>
+   * @since 9.6
+   */
+  public void dismissNotice(String notice) {
+    call(
+      new PostRequest(path("dismiss_notice"))
+        .setParam("notice", notice)
+    ).content();
+  }
+
 }
index 52579c59150686d865936197c0352b56b5dc7b35..462aa020fc81b1e8d002495c9a0a6ec86835361d 100644 (file)
@@ -113,6 +113,7 @@ message CurrentWsResponse {
   reserved 15; // settings removed
   optional bool usingSonarLintConnectedMode = 16;
   optional bool sonarLintAdSeen = 17;
+  map<string,bool> dismissedNotices = 18;
 
   message Permissions {
     repeated string global = 1;