Browse Source

SONAR-13862 Add a WS for dismissing analysis warning by the current user

tags/8.5.0.37579
Michal Duda 3 years ago
parent
commit
0ecd861055
61 changed files with 1775 additions and 226 deletions
  1. 14
    6
      server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/language/HandleUnanalyzedLanguagesStep.java
  2. 22
    16
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/language/HandleUnanalyzedLanguagesStepTest.java
  3. 10
    3
      server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistAnalysisWarningsStepTest.java
  4. 8
    7
      server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java
  5. 5
    5
      server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java
  6. 1
    0
      server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java
  7. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java
  8. 7
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java
  9. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java
  10. 9
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageDao.java
  11. 12
    12
      server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageDto.java
  12. 4
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageMapper.java
  13. 35
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageType.java
  14. 7
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java
  15. 1
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java
  16. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java
  17. 92
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessageDto.java
  18. 63
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessagesDao.java
  19. 37
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessagesMapper.java
  20. 19
    6
      server/sonar-db-dao/src/main/resources/org/sonar/db/ce/CeTaskMessageMapper.xml
  21. 4
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml
  22. 60
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserDismissedMessagesMapper.xml
  23. 14
    1
      server/sonar-db-dao/src/schema/schema-sq.ddl
  24. 1
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/ce/CeActivityDaoTest.java
  25. 49
    5
      server/sonar-db-dao/src/test/java/org/sonar/db/ce/CeTaskMessageDaoTest.java
  26. 17
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeCommandsTest.java
  27. 27
    5
      server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java
  28. 152
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDismissedMessagesDaoTest.java
  29. 13
    0
      server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java
  30. 42
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTable.java
  31. 8
    8
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTable.java
  32. 102
    0
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/CreateUserDismissedMessagesTable.java
  33. 6
    4
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java
  34. 9
    12
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTable.java
  35. 6
    8
      server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTable.java
  36. 7
    9
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest.java
  37. 63
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTableTest.java
  38. 55
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/CreateUserDismissedMessagesTableTest.java
  39. 6
    7
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest.java
  40. 0
    87
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateIsDismissibleColumnOfCeTaskMessageTableTest.java
  41. 73
    0
      server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTableTest.java
  42. 9
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest/schema.sql
  43. 9
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTableTest/schema.sql
  44. 9
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest/schema.sql
  45. 9
    0
      server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTableTest/schema.sql
  46. 70
    0
      server/sonar-webserver-core/src/main/java/org/sonar/server/startup/UpgradeSuggestionsCleaner.java
  47. 148
    0
      server/sonar-webserver-core/src/test/java/org/sonar/server/startup/UpgradeSuggestionsCleanerTest.java
  48. 1
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/AnalysisStatusAction.java
  49. 2
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/CeWsModule.java
  50. 115
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/DismissAnalysisWarningAction.java
  51. 1
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java
  52. 2
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/ActivityActionTest.java
  53. 6
    5
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/AnalysisStatusActionTest.java
  54. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java
  55. 2
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/ComponentActionTest.java
  56. 208
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/DismissAnalysisWarningActionTest.java
  57. 7
    4
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/TaskActionTest.java
  58. 32
    8
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java
  59. 3
    1
      server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java
  60. 16
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java
  61. 59
    0
      sonar-ws/src/main/java/org/sonarqube/ws/client/ce/DismissAnalysisWarningRequest.java

+ 14
- 6
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/language/HandleUnanalyzedLanguagesStep.java View File

@@ -36,6 +36,7 @@ import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
import org.sonar.ce.task.step.ComputationStep;
import org.sonar.core.platform.EditionProvider;
import org.sonar.core.platform.PlatformEditionProvider;
import org.sonar.db.ce.CeTaskMessageType;

import static java.lang.String.format;
import static org.sonar.core.language.UnanalyzedLanguages.C;
@@ -50,9 +51,9 @@ public class HandleUnanalyzedLanguagesStep implements ComputationStep {

static final String DESCRIPTION = "Check upgrade possibility for not analyzed code files.";

private static final String LANGUAGE_UPGRADE_MESSAGE = "%s file(s) detected during the last analysis. %s code cannot be analyzed with SonarQube " +
"community edition. Please consider <a href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to " +
"the Developer Edition</a> to analyze this language.";
private static final String LANGUAGE_UPGRADE_MESSAGE = "%s detected in this project during the last analysis. %s cannot be analyzed with your" +
" current SonarQube edition. Please consider <a target=\"_blank\" href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to" +
" Developer Edition</a> to find Bugs, Code Smells, Vulnerabilities and Security Hotspots in %s.";

private final BatchReportReader reportReader;
private final CeTaskMessages ceTaskMessages;
@@ -102,7 +103,7 @@ public class HandleUnanalyzedLanguagesStep implements ComputationStep {
Iterator<Map.Entry<String, Integer>> iterator = sortedLanguageMap.entrySet().iterator();
Map.Entry<String, Integer> firstLanguage = iterator.next();
StringBuilder languageLabel = new StringBuilder(firstLanguage.getKey());
StringBuilder fileCountLabel = new StringBuilder(format("%s %s", firstLanguage.getValue(), firstLanguage.getKey()));
StringBuilder fileCountLabel = new StringBuilder(format("%s unanalyzed %s", firstLanguage.getValue(), firstLanguage.getKey()));
while (iterator.hasNext()) {
Map.Entry<String, Integer> nextLanguage = iterator.next();
if (iterator.hasNext()) {
@@ -113,10 +114,17 @@ public class HandleUnanalyzedLanguagesStep implements ComputationStep {
fileCountLabel.append(" and ");
}
languageLabel.append(nextLanguage.getKey());
fileCountLabel.append(format("%s %s", nextLanguage.getValue(), nextLanguage.getKey()));
fileCountLabel.append(format("%s unanalyzed %s", nextLanguage.getValue(), nextLanguage.getKey()));
}

return new CeTaskMessages.Message(format(LANGUAGE_UPGRADE_MESSAGE, fileCountLabel, languageLabel), system.now(), true);
if (sortedLanguageMap.size() == 1 && sortedLanguageMap.entrySet().iterator().next().getValue() == 1) {
fileCountLabel.append(" file was");
} else {
fileCountLabel.append(" files were");
}

String message = format(LANGUAGE_UPGRADE_MESSAGE, fileCountLabel, languageLabel, sortedLanguageMap.size() == 1 ? "this file" : "these files");
return new CeTaskMessages.Message(message, system.now(), CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
}

private void computeMeasures(Map<String, Integer> filesPerLanguage) {

+ 22
- 16
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/language/HandleUnanalyzedLanguagesStepTest.java View File

@@ -24,7 +24,10 @@ import java.util.List;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.api.utils.System2;
import org.sonar.ce.task.log.CeTaskMessages;
import org.sonar.ce.task.log.CeTaskMessages.Message;
@@ -37,6 +40,7 @@ import org.sonar.ce.task.projectanalysis.metric.MetricRepositoryRule;
import org.sonar.ce.task.step.TestComputationStepContext;
import org.sonar.core.platform.EditionProvider;
import org.sonar.core.platform.PlatformEditionProvider;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.scanner.protocol.output.ScannerReport;

import static com.google.common.collect.ImmutableList.of;
@@ -54,6 +58,7 @@ import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP;
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_CPP_KEY;
import static org.sonar.server.metric.UnanalyzedLanguageMetrics.UNANALYZED_C_KEY;

@RunWith(MockitoJUnitRunner.class)
public class HandleUnanalyzedLanguagesStepTest {

private static final int PROJECT_REF = 1;
@@ -70,6 +75,9 @@ public class HandleUnanalyzedLanguagesStepTest {
@Rule
public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);

@Captor
private ArgumentCaptor<Message> argumentCaptor;

private final PlatformEditionProvider editionProvider = mock(PlatformEditionProvider.class);
private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);

@@ -93,18 +101,18 @@ public class HandleUnanalyzedLanguagesStepTest {
.putNotAnalyzedFilesByLanguage("C", 10)
.putNotAnalyzedFilesByLanguage("SomeLang", 1000)
.build());
ArgumentCaptor<Message> argumentCaptor = ArgumentCaptor.forClass(Message.class);

underTest.execute(new TestComputationStepContext());

verify(ceTaskMessages, times(1)).add(argumentCaptor.capture());
assertThat(argumentCaptor.getAllValues())
.extracting(Message::getText, Message::isDismissible)
.extracting(Message::getText, Message::getType)
.containsExactly(tuple(
"10 C, 20 C++ and 1000 SomeLang file(s) detected during the last analysis. C, C++ and SomeLang code cannot be analyzed with SonarQube community " +
"edition. Please consider <a href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to the Developer " +
"Edition</a> to analyze this language.",
true));
"10 unanalyzed C, 20 unanalyzed C++ and 1000 unanalyzed SomeLang files were detected in this project during the last analysis. C," +
" C++ and SomeLang cannot be analyzed with your current SonarQube edition. Please consider" +
" <a target=\"_blank\" href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to Developer Edition</a> to find Bugs," +
" Code Smells, Vulnerabilities and Security Hotspots in these files.",
CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE));
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_C_KEY).get().getIntValue()).isEqualTo(10);
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_CPP_KEY).get().getIntValue()).isEqualTo(20);
}
@@ -115,16 +123,15 @@ public class HandleUnanalyzedLanguagesStepTest {
reportReader.setMetadata(ScannerReport.Metadata.newBuilder()
.putNotAnalyzedFilesByLanguage("C", 10)
.build());
ArgumentCaptor<CeTaskMessages.Message> argumentCaptor = ArgumentCaptor.forClass(CeTaskMessages.Message.class);

underTest.execute(new TestComputationStepContext());

verify(ceTaskMessages, times(1)).add(argumentCaptor.capture());
List<CeTaskMessages.Message> messages = argumentCaptor.getAllValues();
assertThat(messages).extracting(CeTaskMessages.Message::getText).containsExactly(
"10 C file(s) detected during the last analysis. C code cannot be analyzed with SonarQube community " +
"edition. Please consider <a href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to the Developer " +
"Edition</a> to analyze this language.");
"10 unanalyzed C files were detected in this project during the last analysis. C cannot be analyzed with your current SonarQube edition. Please" +
" consider <a target=\"_blank\" href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to Developer" +
" Edition</a> to find Bugs, Code Smells, Vulnerabilities and Security Hotspots in this file.");
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_C_KEY).get().getIntValue()).isEqualTo(10);
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_CPP_KEY)).isEmpty();
}
@@ -133,19 +140,18 @@ public class HandleUnanalyzedLanguagesStepTest {
public void adds_warning_in_SQ_community_edition_if_there_are_cpp_files() {
when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.COMMUNITY));
reportReader.setMetadata(ScannerReport.Metadata.newBuilder()
.putNotAnalyzedFilesByLanguage("C++", 9)
.putNotAnalyzedFilesByLanguage("C++", 1)
.build());
ArgumentCaptor<CeTaskMessages.Message> argumentCaptor = ArgumentCaptor.forClass(CeTaskMessages.Message.class);

underTest.execute(new TestComputationStepContext());

verify(ceTaskMessages, times(1)).add(argumentCaptor.capture());
List<CeTaskMessages.Message> messages = argumentCaptor.getAllValues();
assertThat(messages).extracting(CeTaskMessages.Message::getText).containsExactly(
"9 C++ file(s) detected during the last analysis. C++ code cannot be analyzed with SonarQube community " +
"edition. Please consider <a href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to the Developer " +
"Edition</a> to analyze this language.");
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_CPP_KEY).get().getIntValue()).isEqualTo(9);
"1 unanalyzed C++ file was detected in this project during the last analysis. C++ cannot be analyzed with your current SonarQube edition. Please" +
" consider <a target=\"_blank\" href=\"https://www.sonarqube.org/trial-request/developer-edition/?referrer=sonarqube-cpp\">upgrading to Developer" +
" Edition</a> to find Bugs, Code Smells, Vulnerabilities and Security Hotspots in this file.");
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_CPP_KEY).get().getIntValue()).isEqualTo(1);
assertThat(measureRepository.getAddedRawMeasure(PROJECT_REF, UNANALYZED_C_KEY)).isEmpty();
}


+ 10
- 3
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/step/PersistAnalysisWarningsStepTest.java View File

@@ -23,10 +23,14 @@ import com.google.common.collect.ImmutableList;
import java.util.List;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.junit.MockitoJUnitRunner;
import org.sonar.ce.task.log.CeTaskMessages;
import org.sonar.ce.task.projectanalysis.batch.BatchReportReaderRule;
import org.sonar.ce.task.step.TestComputationStepContext;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.scanner.protocol.output.ScannerReport;

import static com.google.common.collect.ImmutableList.of;
@@ -38,11 +42,15 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;

@RunWith(MockitoJUnitRunner.class)
public class PersistAnalysisWarningsStepTest {

@Rule
public BatchReportReaderRule reportReader = new BatchReportReaderRule();

@Captor
private ArgumentCaptor<List<CeTaskMessages.Message>> argumentCaptor;

private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);
private final PersistAnalysisWarningsStep underTest = new PersistAnalysisWarningsStep(reportReader, ceTaskMessages);

@@ -57,14 +65,13 @@ public class PersistAnalysisWarningsStepTest {
ScannerReport.AnalysisWarning warning2 = ScannerReport.AnalysisWarning.newBuilder().setText("warning 2").build();
ImmutableList<ScannerReport.AnalysisWarning> warnings = of(warning1, warning2);
reportReader.setAnalysisWarnings(warnings);
ArgumentCaptor<List<CeTaskMessages.Message>> argumentCaptor = ArgumentCaptor.forClass(List.class);

underTest.execute(new TestComputationStepContext());

verify(ceTaskMessages, times(1)).addAll(argumentCaptor.capture());
assertThat(argumentCaptor.getValue())
.extracting(CeTaskMessages.Message::getText, CeTaskMessages.Message::isDismissible)
.containsExactly(tuple("warning 1", false), tuple("warning 2", false));
.extracting(CeTaskMessages.Message::getText, CeTaskMessages.Message::getType)
.containsExactly(tuple("warning 1", CeTaskMessageType.GENERIC), tuple("warning 2", CeTaskMessageType.GENERIC));
}

@Test

+ 8
- 7
server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessages.java View File

@@ -23,6 +23,7 @@ import java.util.Collection;
import java.util.Objects;
import javax.annotation.concurrent.Immutable;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.db.ce.CeTaskMessageType;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;
@@ -48,19 +49,19 @@ public interface CeTaskMessages {
class Message {
private final String text;
private final long timestamp;
private final boolean dismissible;
private final CeTaskMessageType type;

public Message(String text, long timestamp, boolean dismissible) {
public Message(String text, long timestamp, CeTaskMessageType type) {
requireNonNull(text, "Text can't be null");
checkArgument(!text.isEmpty(), "Text can't be empty");
checkArgument(timestamp >= 0, "Timestamp can't be less than 0");
this.text = text;
this.timestamp = timestamp;
this.dismissible = dismissible;
this.type = type;
}

public Message(String text, long timestamp) {
this(text, timestamp, false);
this(text, timestamp, CeTaskMessageType.GENERIC);
}

public String getText() {
@@ -71,8 +72,8 @@ public interface CeTaskMessages {
return timestamp;
}

public boolean isDismissible() {
return dismissible;
public CeTaskMessageType getType() {
return type;
}

@Override
@@ -98,7 +99,7 @@ public interface CeTaskMessages {
return "Message{" +
"text='" + text + '\'' +
", timestamp=" + timestamp +
", dismissible=" + dismissible +
", type=" + type +
'}';
}
}

+ 5
- 5
server/sonar-ce-task/src/main/java/org/sonar/ce/task/log/CeTaskMessagesImpl.java View File

@@ -71,11 +71,11 @@ public class CeTaskMessagesImpl implements CeTaskMessages {

public void insert(DbSession dbSession, Message message) {
dbClient.ceTaskMessageDao().insert(dbSession, new CeTaskMessageDto()
.setUuid(uuidFactory.create())
.setTaskUuid(ceTask.getUuid())
.setMessage(message.getText())
.setDismissible(message.isDismissible())
.setCreatedAt(message.getTimestamp()));
.setUuid(uuidFactory.create())
.setTaskUuid(ceTask.getUuid())
.setMessage(message.getText())
.setType(message.getType())
.setCreatedAt(message.getTimestamp()));
}

private static void checkMessage(Message message) {

+ 1
- 0
server/sonar-db-core/src/main/java/org/sonar/db/version/SqTables.java View File

@@ -115,6 +115,7 @@ public final class SqTables {
"session_tokens",
"snapshots",
"users",
"user_dismissed_messages",
"user_properties",
"user_roles",
"user_tokens",

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/DaoModule.java View File

@@ -86,6 +86,7 @@ import org.sonar.db.user.RoleDao;
import org.sonar.db.user.SamlMessageIdDao;
import org.sonar.db.user.SessionTokensDao;
import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserDismissedMessagesDao;
import org.sonar.db.user.UserGroupDao;
import org.sonar.db.user.UserPropertiesDao;
import org.sonar.db.user.UserTokenDao;
@@ -159,6 +160,7 @@ public class DaoModule extends Module {
SchemaMigrationDao.class,
SessionTokensDao.class,
UserDao.class,
UserDismissedMessagesDao.class,
UserGroupDao.class,
UserPermissionDao.class,
UserPropertiesDao.class,

+ 7
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/DbClient.java View File

@@ -84,6 +84,7 @@ import org.sonar.db.user.RoleDao;
import org.sonar.db.user.SamlMessageIdDao;
import org.sonar.db.user.SessionTokensDao;
import org.sonar.db.user.UserDao;
import org.sonar.db.user.UserDismissedMessagesDao;
import org.sonar.db.user.UserGroupDao;
import org.sonar.db.user.UserPropertiesDao;
import org.sonar.db.user.UserTokenDao;
@@ -164,6 +165,7 @@ public class DbClient {
private final ProjectDao projectDao;
private final SessionTokensDao sessionTokensDao;
private final SamlMessageIdDao samlMessageIdDao;
private final UserDismissedMessagesDao userDismissedMessagesDao;

public DbClient(Database database, MyBatis myBatis, DBSessions dbSessions, Dao... daos) {
this.database = database;
@@ -242,6 +244,7 @@ public class DbClient {
projectDao = getDao(map, ProjectDao.class);
sessionTokensDao = getDao(map, SessionTokensDao.class);
samlMessageIdDao = getDao(map, SamlMessageIdDao.class);
userDismissedMessagesDao = getDao(map, UserDismissedMessagesDao.class);
}

public DbSession openSession(boolean batch) {
@@ -534,4 +537,8 @@ public class DbClient {
return samlMessageIdDao;
}

public UserDismissedMessagesDao userDismissedMessagesDao() {
return userDismissedMessagesDao;
}

}

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/MyBatis.java View File

@@ -140,6 +140,7 @@ import org.sonar.db.user.GroupMembershipMapper;
import org.sonar.db.user.RoleMapper;
import org.sonar.db.user.SamlMessageIdMapper;
import org.sonar.db.user.SessionTokenMapper;
import org.sonar.db.user.UserDismissedMessagesMapper;
import org.sonar.db.user.UserDto;
import org.sonar.db.user.UserGroupDto;
import org.sonar.db.user.UserGroupMapper;
@@ -287,6 +288,7 @@ public class MyBatis implements Startable {
SchemaMigrationMapper.class,
SessionTokenMapper.class,
SnapshotMapper.class,
UserDismissedMessagesMapper.class,
UserGroupMapper.class,
UserMapper.class,
UserPermissionMapper.class,

+ 9
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageDao.java View File

@@ -20,6 +20,7 @@
package org.sonar.db.ce;

import java.util.List;
import java.util.Optional;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;

@@ -28,6 +29,10 @@ public class CeTaskMessageDao implements Dao {
getMapper(dbSession).insert(dto);
}

public Optional<CeTaskMessageDto> selectByUuid(DbSession dbSession, String uuid) {
return getMapper(dbSession).selectByUuid(uuid);
}

/**
* @return the messages for the specific task, if any, in ascending order of column {@code CREATED_AT}.
*/
@@ -35,6 +40,10 @@ public class CeTaskMessageDao implements Dao {
return getMapper(dbSession).selectByTask(taskUuid);
}

public void deleteByType(DbSession session, CeTaskMessageType type) {
getMapper(session).deleteByType(type.name());
}

private static CeTaskMessageMapper getMapper(DbSession dbSession) {
return dbSession.getMapper(CeTaskMessageMapper.class);
}

+ 12
- 12
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageDto.java View File

@@ -35,13 +35,13 @@ public class CeTaskMessageDto {
*/
private String message;
/**
* Timestamp the message was created. Not null
* Type of the message
*/
private long createdAt;
private CeTaskMessageType type;
/**
* Information if this message can be dismissed by the user
* Timestamp the message was created. Not null
*/
private boolean dismissible;
private long createdAt;

public String getUuid() {
return uuid;
@@ -72,21 +72,21 @@ public class CeTaskMessageDto {
return this;
}

public long getCreatedAt() {
return createdAt;
public CeTaskMessageType getType() {
return type;
}

public CeTaskMessageDto setCreatedAt(long createdAt) {
this.createdAt = createdAt;
public CeTaskMessageDto setType(CeTaskMessageType type) {
this.type = type;
return this;
}

public boolean isDismissible() {
return dismissible;
public long getCreatedAt() {
return createdAt;
}

public CeTaskMessageDto setDismissible(boolean dismissible) {
this.dismissible = dismissible;
public CeTaskMessageDto setCreatedAt(long createdAt) {
this.createdAt = createdAt;
return this;
}
}

+ 4
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageMapper.java View File

@@ -20,11 +20,15 @@
package org.sonar.db.ce;

import java.util.List;
import java.util.Optional;
import org.apache.ibatis.annotations.Param;

public interface CeTaskMessageMapper {
void insert(@Param("dto") CeTaskMessageDto dto);

Optional<CeTaskMessageDto> selectByUuid(@Param("uuid") String uuid);

List<CeTaskMessageDto> selectByTask(@Param("taskUuid") String taskUuid);

void deleteByType(@Param("ceMessageType") String ceMessageType);
}

+ 35
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/ce/CeTaskMessageType.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.ce;

public enum CeTaskMessageType {
GENERIC(false),
SUGGEST_DEVELOPER_EDITION_UPGRADE(true);

private final boolean dismissible;

CeTaskMessageType(boolean dismissible) {
this.dismissible = dismissible;
}

public boolean isDismissible() {
return dismissible;
}
}

+ 7
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeCommands.java View File

@@ -434,4 +434,11 @@ class PurgeCommands {
profiler.stop();
}

void deleteUserDismissedMessages(String projectUuid) {
profiler.start("deleteUserDismissedMessages (user_dismissed_messages)");
purgeMapper.deleteUserDismissedMessagesByProjectUuid(projectUuid);
session.commit();
profiler.stop();
}

}

+ 1
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeDao.java View File

@@ -224,6 +224,7 @@ public class PurgeDao implements Dao {
commands.deleteComponents(rootUuid);
commands.deleteComponentsByMainBranchProjectUuid(rootUuid);
commands.deleteProject(rootUuid);
commands.deleteUserDismissedMessages(rootUuid);
}

/**

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/purge/PurgeMapper.java View File

@@ -150,4 +150,6 @@ public interface PurgeMapper {
void deleteNewCodePeriodsByRootUuid(String rootUuid);

void deleteProjectAlmSettingsByProjectUuid(@Param("projectUuid") String projectUuid);

void deleteUserDismissedMessagesByProjectUuid(@Param("projectUuid") String projectUuid);
}

+ 92
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessageDto.java View File

@@ -0,0 +1,92 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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 org.sonar.db.ce.CeTaskMessageType;

public class UserDismissedMessageDto {

private String uuid;
/**
* Uuid of the user that dismissed the message type
*/
private String userUuid;
/**
* Uuid of the project for which the message type was dismissed
*/
private String projectUuid;
/**
* Message type of the dismissed message
*/
private CeTaskMessageType ceMessageType;
/**
* Technical creation date
*/
private long createdAt;

public UserDismissedMessageDto() {
// nothing to do here
}

public String getUuid() {
return uuid;
}

public UserDismissedMessageDto setUuid(String uuid) {
this.uuid = uuid;
return this;
}

public String getUserUuid() {
return userUuid;
}

public UserDismissedMessageDto setUserUuid(String userUuid) {
this.userUuid = userUuid;
return this;
}

public String getProjectUuid() {
return projectUuid;
}

public UserDismissedMessageDto setProjectUuid(String projectUuid) {
this.projectUuid = projectUuid;
return this;
}

public CeTaskMessageType getCeMessageType() {
return ceMessageType;
}

public UserDismissedMessageDto setCeMessageType(CeTaskMessageType ceMessageType) {
this.ceMessageType = ceMessageType;
return this;
}

public long getCreatedAt() {
return createdAt;
}

public UserDismissedMessageDto setCreatedAt(long createdAt) {
this.createdAt = createdAt;
return this;
}
}

+ 63
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessagesDao.java View File

@@ -0,0 +1,63 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.Optional;
import org.sonar.api.utils.System2;
import org.sonar.db.Dao;
import org.sonar.db.DbSession;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.project.ProjectDto;

public class UserDismissedMessagesDao implements Dao {
private final System2 system2;

public UserDismissedMessagesDao(System2 system2) {
this.system2 = system2;
}

public UserDismissedMessageDto insert(DbSession session, UserDismissedMessageDto dto) {
long now = system2.now();
mapper(session).insert(dto.setCreatedAt(now));
return dto;
}

public Optional<UserDismissedMessageDto> selectByUserAndProjectAndMessageType(DbSession session, UserDto user, ProjectDto project,
CeTaskMessageType ceMessageType) {
return mapper(session).selectByUserUuidAndProjectUuidAndMessageType(user.getUuid(), project.getUuid(), ceMessageType.name());
}

public List<UserDismissedMessageDto> selectByUser(DbSession session, UserDto user) {
return mapper(session).selectByUserUuid(user.getUuid());
}

public void deleteByUser(DbSession session, UserDto user) {
mapper(session).deleteByUserUuid(user.getUuid());
}

public void deleteByType(DbSession session, CeTaskMessageType type) {
mapper(session).deleteByType(type.name());
}

private static UserDismissedMessagesMapper mapper(DbSession session) {
return session.getMapper(UserDismissedMessagesMapper.class);
}
}

+ 37
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/user/UserDismissedMessagesMapper.java View File

@@ -0,0 +1,37 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.Optional;
import org.apache.ibatis.annotations.Param;

public interface UserDismissedMessagesMapper {
void insert(@Param("dto") UserDismissedMessageDto dto);

Optional<UserDismissedMessageDto> selectByUserUuidAndProjectUuidAndMessageType(@Param("userUuid") String userUuid, @Param("projectUuid") String projectUuid,
@Param("ceMessageType") String ceMessageType);

List<UserDismissedMessageDto> selectByUserUuid(@Param("userUuid") String userUuid);

void deleteByUserUuid(@Param("userUuid") String userUuid);

void deleteByType(@Param("ceMessageType") String ceMessageType);
}

+ 19
- 6
server/sonar-db-dao/src/main/resources/org/sonar/db/ce/CeTaskMessageMapper.xml View File

@@ -7,10 +7,19 @@
ctm.uuid,
ctm.task_uuid as taskUuid,
ctm.message as message,
ctm.created_at as createdAt,
ctm.is_dismissible as dismissible
ctm.message_type as type,
ctm.created_at as createdAt
</sql>

<select id="selectByUuid" resultType="org.sonar.db.ce.CeTaskMessageDto">
select
<include refid="columns"/>
from
ce_task_message ctm
where
ctm.uuid=#{uuid,jdbcType=VARCHAR}
</select>

<select id="selectByTask" resultType="org.sonar.db.ce.CeTaskMessageDto">
select
<include refid="columns"/>
@@ -28,16 +37,20 @@
uuid,
task_uuid,
message,
created_at,
is_dismissible
message_type,
created_at
)
values (
#{dto.uuid,jdbcType=VARCHAR},
#{dto.taskUuid,jdbcType=VARCHAR},
#{dto.message,jdbcType=VARCHAR},
#{dto.createdAt,jdbcType=BIGINT},
#{dto.dismissible,jdbcType=BOOLEAN}
#{dto.type,jdbcType=VARCHAR},
#{dto.createdAt,jdbcType=BIGINT}
)
</insert>

<delete id="deleteByType" parameterType="String">
delete from ce_task_message where message_type = #{ceMessageType, jdbcType=VARCHAR}
</delete>

</mapper>

+ 4
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/purge/PurgeMapper.xml View File

@@ -504,5 +504,9 @@
<delete id="deleteLiveMeasuresByComponentUuids">
delete from live_measures where component_uuid in <foreach item="componentUuid" index="index" collection="componentUuids" open="(" separator="," close=")">#{componentUuid, jdbcType=VARCHAR}</foreach>
</delete>

<delete id="deleteUserDismissedMessagesByProjectUuid">
delete from user_dismissed_messages where project_uuid = #{projectUuid,jdbcType=VARCHAR}
</delete>
</mapper>


+ 60
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/user/UserDismissedMessagesMapper.xml View File

@@ -0,0 +1,60 @@
<?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.UserDismissedMessagesMapper">

<sql id="columns">
udm.uuid,
udm.user_uuid as userUuid,
udm.project_uuid as projectUuid,
udm.message_type as ceMessageType,
udm.created_at as createdAt
</sql>

<insert id="insert" parameterType="org.sonar.db.user.UserDismissedMessageDto" useGeneratedKeys="false">
insert into user_dismissed_messages
(
uuid,
user_uuid,
project_uuid,
message_type,
created_at
)
values (
#{dto.uuid,jdbcType=VARCHAR},
#{dto.userUuid,jdbcType=VARCHAR},
#{dto.projectUuid,jdbcType=VARCHAR},
#{dto.ceMessageType,jdbcType=VARCHAR},
#{dto.createdAt,jdbcType=BIGINT}
)
</insert>

<select id="selectByUserUuidAndProjectUuidAndMessageType" resultType="org.sonar.db.user.UserDismissedMessageDto">
select
<include refid="columns"/>
from
user_dismissed_messages udm
where
udm.user_uuid=#{userUuid,jdbcType=VARCHAR} and
udm.project_uuid=#{projectUuid,jdbcType=VARCHAR} and
udm.message_type=#{ceMessageType,jdbcType=VARCHAR}
</select>

<select id="selectByUserUuid" resultType="org.sonar.db.user.UserDismissedMessageDto" parameterType="String">
select
<include refid="columns"/>
from
user_dismissed_messages udm
where
udm.user_uuid=#{userUuid,jdbcType=VARCHAR}
</select>

<delete id="deleteByUserUuid" parameterType="String">
delete from user_dismissed_messages where user_uuid = #{userUuid, jdbcType=VARCHAR}
</delete>

<delete id="deleteByType" parameterType="String">
delete from user_dismissed_messages where message_type = #{ceMessageType, jdbcType=VARCHAR}
</delete>

</mapper>

+ 14
- 1
server/sonar-db-dao/src/schema/schema-sq.ddl View File

@@ -169,10 +169,11 @@ CREATE TABLE "CE_TASK_MESSAGE"(
"TASK_UUID" VARCHAR(40) NOT NULL,
"MESSAGE" VARCHAR(4000) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
"IS_DISMISSIBLE" BOOLEAN NOT NULL
"MESSAGE_TYPE" VARCHAR(255) NOT NULL
);
ALTER TABLE "CE_TASK_MESSAGE" ADD CONSTRAINT "PK_CE_TASK_MESSAGE" PRIMARY KEY("UUID");
CREATE INDEX "CE_TASK_MESSAGE_TASK" ON "CE_TASK_MESSAGE"("TASK_UUID");
CREATE INDEX "CTM_MESSAGE_TYPE" ON "CE_TASK_MESSAGE"("MESSAGE_TYPE");

CREATE TABLE "COMPONENTS"(
"UUID" VARCHAR(50) NOT NULL,
@@ -896,6 +897,18 @@ ALTER TABLE "SNAPSHOTS" ADD CONSTRAINT "PK_SNAPSHOTS" PRIMARY KEY("UUID");
CREATE UNIQUE INDEX "ANALYSES_UUID" ON "SNAPSHOTS"("UUID");
CREATE INDEX "SNAPSHOT_COMPONENT" ON "SNAPSHOTS"("COMPONENT_UUID");

CREATE TABLE "USER_DISMISSED_MESSAGES"(
"UUID" VARCHAR(40) NOT NULL,
"USER_UUID" VARCHAR(255) NOT NULL,
"PROJECT_UUID" VARCHAR(40) NOT NULL,
"MESSAGE_TYPE" VARCHAR(255) NOT NULL,
"CREATED_AT" BIGINT NOT NULL
);
ALTER TABLE "USER_DISMISSED_MESSAGES" ADD CONSTRAINT "PK_USER_DISMISSED_MESSAGES" PRIMARY KEY("UUID");
CREATE UNIQUE INDEX "UNIQ_USER_DISMISSED_MESSAGES" ON "USER_DISMISSED_MESSAGES"("USER_UUID", "PROJECT_UUID", "MESSAGE_TYPE");
CREATE INDEX "UDM_PROJECT_UUID" ON "USER_DISMISSED_MESSAGES"("PROJECT_UUID");
CREATE INDEX "UDM_MESSAGE_TYPE" ON "USER_DISMISSED_MESSAGES"("MESSAGE_TYPE");

CREATE TABLE "USER_PROPERTIES"(
"UUID" VARCHAR(40) NOT NULL,
"USER_UUID" VARCHAR(255) NOT NULL,

+ 1
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/ce/CeActivityDaoTest.java View File

@@ -139,6 +139,7 @@ public class CeActivityDaoTest {
.setUuid(UuidFactoryFast.getInstance().create())
.setTaskUuid(task.getUuid())
.setMessage("message_" + task.getUuid() + "_" + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(task.getUuid().hashCode() + i)));
db.commit();
}

+ 49
- 5
server/sonar-db-dao/src/test/java/org/sonar/db/ce/CeTaskMessageDaoTest.java View File

@@ -20,6 +20,7 @@
package org.sonar.db.ce;

import java.util.List;
import java.util.Optional;
import org.assertj.core.groups.Tuple;
import org.junit.Rule;
import org.junit.Test;
@@ -37,7 +38,7 @@ public class CeTaskMessageDaoTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();

private CeTaskMessageDao underTest = new CeTaskMessageDao();
private final CeTaskMessageDao underTest = new CeTaskMessageDao();

@Test
public void insert() {
@@ -45,13 +46,17 @@ public class CeTaskMessageDaoTest {
.setUuid("uuid_1")
.setTaskUuid("task_uuid_1")
.setMessage("message_1")
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(1_222_333L));
dbTester.getSession().commit();

assertThat(dbTester.select("select uuid as \"UUID\", task_uuid as \"TASK_UUID\", message as \"MESSAGE\", created_at as \"CREATED_AT\" from ce_task_message"))
.hasSize(1)
.extracting(t -> t.get("UUID"), t -> t.get("TASK_UUID"), t -> t.get("MESSAGE"), t -> t.get("CREATED_AT"))
.containsOnly(Tuple.tuple("uuid_1", "task_uuid_1", "message_1", 1_222_333L));
assertThat(
dbTester.select("select uuid as \"UUID\", task_uuid as \"TASK_UUID\", message as \"MESSAGE\", message_type as \"TYPE\", " +
"created_at as \"CREATED_AT\" from ce_task_message"))
.hasSize(1)
.extracting(t -> t.get("UUID"), t -> t.get("TASK_UUID"), t -> t.get("MESSAGE"), t -> CeTaskMessageType.valueOf((String) t.get("TYPE")),
t -> t.get("CREATED_AT"))
.containsOnly(Tuple.tuple("uuid_1", "task_uuid_1", "message_1", CeTaskMessageType.GENERIC, 1_222_333L));
}

@Test
@@ -88,11 +93,50 @@ public class CeTaskMessageDaoTest {
.isEmpty();
}

@Test
public void selectByUuid_returns_object_if_found() {
CeTaskMessageDto dto = insertMessage("526787a4-e8af-46c0-b340-8c48188646a5", 1, 1_222_333L);

Optional<CeTaskMessageDto> result = underTest.selectByUuid(dbTester.getSession(), dto.getUuid());

assertThat(result).isPresent();
assertThat(result.get().getUuid()).isEqualTo(dto.getUuid());
}

@Test
public void selectByUuid_returns_empty_if_no_record_found() {
Optional<CeTaskMessageDto> result = underTest.selectByUuid(dbTester.getSession(), "e2a71626-1f07-402a-aac7-dd4e0bbb4394");

assertThat(result).isNotPresent();
}

@Test
public void deleteByType_deletes_messages_of_given_type() {
String task1 = "task1";
CeTaskMessageDto[] messages = {
insertMessage(task1, 0, 1_222_333L, CeTaskMessageType.GENERIC),
insertMessage(task1, 1, 2_222_333L, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE),
insertMessage(task1, 2, 1_111_333L, CeTaskMessageType.GENERIC),
insertMessage(task1, 3, 1_222_111L, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE)
};

underTest.deleteByType(dbTester.getSession(), CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

assertThat(underTest.selectByTask(dbTester.getSession(), task1))
.extracting(CeTaskMessageDto::getUuid)
.containsExactlyInAnyOrder(messages[0].getUuid(), messages[2].getUuid());
}

private CeTaskMessageDto insertMessage(String taskUuid, int i, long createdAt) {
return insertMessage(taskUuid, i, createdAt, CeTaskMessageType.GENERIC);
}

private CeTaskMessageDto insertMessage(String taskUuid, int i, long createdAt, CeTaskMessageType messageType) {
CeTaskMessageDto res = new CeTaskMessageDto()
.setUuid("message_" + i)
.setTaskUuid(taskUuid)
.setMessage("test_" + i)
.setType(messageType)
.setCreatedAt(createdAt);
DbSession dbSession = dbTester.getSession();
underTest.insert(dbSession, res);

+ 17
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeCommandsTest.java View File

@@ -36,6 +36,7 @@ import org.sonar.api.impl.utils.AlwaysIncreasingSystem2;
import org.sonar.api.utils.System2;
import org.sonar.core.util.UuidFactoryFast;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.ComponentTesting;
@@ -625,6 +626,22 @@ public class PurgeCommandsTest {
assertThat(dbTester.countRowsOfTable("new_code_periods")).isEqualTo(3);
}

@Test
public void deleteUserDismissedMessages_deletes_dismissed_warnings_on_project_for_all_users() {
UserDto user1 = dbTester.users().insertUser();
UserDto user2 = dbTester.users().insertUser();
ProjectDto project = dbTester.components().insertPrivateProjectDto();
ProjectDto anotherProject = dbTester.components().insertPrivateProjectDto();
dbTester.users().insertUserDismissedMessage(user1, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
dbTester.users().insertUserDismissedMessage(user2, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
dbTester.users().insertUserDismissedMessage(user1, anotherProject, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
PurgeCommands purgeCommands = new PurgeCommands(dbTester.getSession(), profiler, system2);

purgeCommands.deleteUserDismissedMessages(project.getUuid());

assertThat(dbTester.countRowsOfTable("user_dismissed_messages")).isEqualTo(1);
}

private void addPermissions(OrganizationDto organization, ComponentDto root) {
if (!root.isPrivate()) {
dbTester.users().insertProjectPermissionOnAnyone("foo1", root);

+ 27
- 5
server/sonar-db-dao/src/test/java/org/sonar/db/purge/PurgeDaoTest.java View File

@@ -47,7 +47,6 @@ import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.alm.ALM;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
@@ -55,6 +54,7 @@ import org.sonar.db.ce.CeQueueDto.Status;
import org.sonar.db.ce.CeTaskCharacteristicDto;
import org.sonar.db.ce.CeTaskInputDao;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.BranchDto;
import org.sonar.db.component.BranchType;
@@ -78,6 +78,8 @@ import org.sonar.db.project.ProjectDto;
import org.sonar.db.property.PropertyDto;
import org.sonar.db.rule.RuleDefinitionDto;
import org.sonar.db.source.FileSourceDto;
import org.sonar.db.user.UserDismissedMessageDto;
import org.sonar.db.user.UserDto;
import org.sonar.db.webhook.WebhookDeliveryLiteDto;
import org.sonar.db.webhook.WebhookDto;

@@ -110,16 +112,16 @@ public class PurgeDaoTest {

private static final String PROJECT_UUID = "P1";

private System2 system2 = mock(System2.class);
private final System2 system2 = mock(System2.class);

@Rule
public DbTester db = DbTester.create(system2);
@Rule
public ExpectedException expectedException = ExpectedException.none();

private DbClient dbClient = db.getDbClient();
private DbSession dbSession = db.getSession();
private PurgeDao underTest = db.getDbClient().purgeDao();
private final DbClient dbClient = db.getDbClient();
private final DbSession dbSession = db.getSession();
private final PurgeDao underTest = db.getDbClient().purgeDao();

@Test
public void purge_failed_ce_tasks() {
@@ -702,6 +704,25 @@ public class PurgeDaoTest {
assertThat(taskUuidsIn("ce_task_message")).containsOnly(anotherProjectTask.getUuid(), "non existing task");
}

@Test
public void delete_rows_in_user_dismissed_messages_when_deleting_project() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();
ProjectDto project = db.components().insertPrivateProjectDto();
ProjectDto anotherProject = db.components().insertPrivateProjectDto();

UserDismissedMessageDto msg1 = db.users().insertUserDismissedMessage(user1, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
UserDismissedMessageDto msg2 = db.users().insertUserDismissedMessage(user2, project, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
UserDismissedMessageDto msg3 = db.users().insertUserDismissedMessage(user1, anotherProject, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

assertThat(uuidsIn("user_dismissed_messages")).containsOnly(msg1.getUuid(), msg2.getUuid(), msg3.getUuid());

underTest.deleteProject(dbSession, project.getUuid());
dbSession.commit();

assertThat(uuidsIn("user_dismissed_messages")).containsOnly(msg3.getUuid());
}

@Test
public void delete_tasks_in_ce_queue_when_deleting_project() {
ComponentDto projectToBeDeleted = db.components().insertPrivateProject();
@@ -1629,6 +1650,7 @@ public class PurgeDaoTest {
.setUuid(UuidFactoryFast.getInstance().create())
.setTaskUuid(uuid)
.setMessage("key_" + uuid.hashCode() + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(2_333_444L + i))
.forEach(dto -> dbClient.ceTaskMessageDao().insert(dbSession, dto));
dbSession.commit();

+ 152
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/user/UserDismissedMessagesDaoTest.java View File

@@ -0,0 +1,152 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.core.util.Uuids;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.project.ProjectDto;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.db.ce.CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE;

public class UserDismissedMessagesDaoTest {

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

private final UserDismissedMessagesDao underTest = db.getDbClient().userDismissedMessagesDao();

@Test
public void insert_user_dismissed_message() {
ProjectDto project = db.components().insertPrivateProjectDto();
UserDto user = db.users().insertUser();
UserDismissedMessageDto dto = newDto(project, user);

underTest.insert(db.getSession(), dto);

Optional<UserDismissedMessageDto> dtoFromDb = underTest.selectByUserAndProjectAndMessageType(db.getSession(), user, project, dto.getCeMessageType());
assertThat(dtoFromDb).isPresent();
assertThat(dtoFromDb.get().getUuid()).isEqualTo(dto.getUuid());
assertThat(dtoFromDb.get().getUserUuid()).isEqualTo(dto.getUserUuid());
assertThat(dtoFromDb.get().getProjectUuid()).isEqualTo(dto.getProjectUuid());
assertThat(dtoFromDb.get().getCeMessageType()).isEqualTo(dto.getCeMessageType());
assertThat(dtoFromDb.get().getCeMessageType().isDismissible()).isEqualTo(dto.getCeMessageType().isDismissible());
assertThat(dtoFromDb.get().getCreatedAt()).isEqualTo(dto.getCreatedAt());
}

@Test
public void selectByUserAndProjectAndMessageType_returns_object_if_record_found() {
UserDto user = db.users().insertUser();
ProjectDto project = db.components().insertPrivateProjectDto();
db.users().insertUserDismissedMessage(user, project, CeTaskMessageType.GENERIC);

Optional<UserDismissedMessageDto> result = underTest.selectByUserAndProjectAndMessageType(db.getSession(), user, project, CeTaskMessageType.GENERIC);

assertThat(result).isPresent();
assertThat(result.get().getUserUuid()).isEqualTo(user.getUuid());
assertThat(result.get().getProjectUuid()).isEqualTo(project.getUuid());
assertThat(result.get().getCeMessageType()).isEqualTo(CeTaskMessageType.GENERIC);
}

@Test
public void selectByUserAndProjectAndMessageType_returns_absent_if_no_record_found() {
UserDto user = db.users().insertUser();
ProjectDto project = db.components().insertPrivateProjectDto();
db.users().insertUserDismissedMessage(user, project, CeTaskMessageType.GENERIC);

Optional<UserDismissedMessageDto> result = underTest.selectByUserAndProjectAndMessageType(db.getSession(), user, project, SUGGEST_DEVELOPER_EDITION_UPGRADE);

assertThat(result).isNotPresent();
}

@Test
public void selectByUserUuid_returns_all_dismissed_messages_of_a_user() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();
ProjectDto project1 = db.components().insertPrivateProjectDto();
ProjectDto project2 = db.components().insertPrivateProjectDto();
db.users().insertUserDismissedMessage(user1, project1, CeTaskMessageType.GENERIC);
db.users().insertUserDismissedMessage(user1, project2, CeTaskMessageType.GENERIC);
UserDismissedMessageDto dto1 = db.users().insertUserDismissedMessage(user2, project1, CeTaskMessageType.GENERIC);
UserDismissedMessageDto dto2 = db.users().insertUserDismissedMessage(user2, project2, CeTaskMessageType.GENERIC);

List<UserDismissedMessageDto> result = underTest.selectByUser(db.getSession(), user2);

assertThat(result).hasSize(2);
assertThat(result).extracting(UserDismissedMessageDto::getUuid, UserDismissedMessageDto::getUserUuid, UserDismissedMessageDto::getProjectUuid,
UserDismissedMessageDto::getCeMessageType)
.containsExactlyInAnyOrder(
tuple(dto1.getUuid(), user2.getUuid(), project1.getUuid(), CeTaskMessageType.GENERIC),
tuple(dto2.getUuid(), user2.getUuid(), project2.getUuid(), CeTaskMessageType.GENERIC));
}

@Test
public void deleteByUserUuid_removes_dismiss_warning_data_of_a_user() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();
ProjectDto project1 = db.components().insertPrivateProjectDto();
ProjectDto project2 = db.components().insertPrivateProjectDto();
db.users().insertUserDismissedMessage(user1, project1, CeTaskMessageType.GENERIC);
db.users().insertUserDismissedMessage(user1, project2, CeTaskMessageType.GENERIC);
db.users().insertUserDismissedMessage(user2, project1, CeTaskMessageType.GENERIC);
db.users().insertUserDismissedMessage(user2, project2, CeTaskMessageType.GENERIC);

underTest.deleteByUser(db.getSession(), user2);

assertThat(underTest.selectByUser(db.getSession(), user1)).hasSize(2);
assertThat(underTest.selectByUser(db.getSession(), user2)).isEmpty();
}

@Test
public void deleteByUserUuid_removes_dismissed_messages_of_that_type() {
UserDto user1 = db.users().insertUser();
UserDto user2 = db.users().insertUser();
ProjectDto project1 = db.components().insertPrivateProjectDto();
ProjectDto project2 = db.components().insertPrivateProjectDto();
UserDismissedMessageDto dto1 = db.users().insertUserDismissedMessage(user1, project1, CeTaskMessageType.GENERIC);
db.users().insertUserDismissedMessage(user1, project2, SUGGEST_DEVELOPER_EDITION_UPGRADE);
db.users().insertUserDismissedMessage(user2, project1, SUGGEST_DEVELOPER_EDITION_UPGRADE);
UserDismissedMessageDto dto2 = db.users().insertUserDismissedMessage(user2, project2, CeTaskMessageType.GENERIC);

underTest.deleteByType(db.getSession(), SUGGEST_DEVELOPER_EDITION_UPGRADE);

assertThat(underTest.selectByUser(db.getSession(), user1))
.extracting(UserDismissedMessageDto::getUuid)
.containsExactly(dto1.getUuid());
assertThat(underTest.selectByUser(db.getSession(), user2))
.extracting(UserDismissedMessageDto::getUuid)
.containsExactly(dto2.getUuid());
}

public static UserDismissedMessageDto newDto(ProjectDto project, UserDto user) {
return new UserDismissedMessageDto()
.setUuid(Uuids.createFast())
.setCeMessageType(CeTaskMessageType.GENERIC)
.setUserUuid(user.getUuid())
.setProjectUuid(project.getUuid());
}
}

+ 13
- 0
server/sonar-db-dao/src/testFixtures/java/org/sonar/db/user/UserDbTester.java View File

@@ -32,11 +32,13 @@ import org.sonar.core.util.Uuids;
import org.sonar.core.util.stream.MoreCollectors;
import org.sonar.db.DbClient;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.permission.GroupPermissionDto;
import org.sonar.db.permission.OrganizationPermission;
import org.sonar.db.permission.UserPermissionDto;
import org.sonar.db.project.ProjectDto;

import static com.google.common.base.Preconditions.checkArgument;
import static java.lang.String.format;
@@ -403,4 +405,15 @@ public class UserDbTester {
return dto;
}

public final UserDismissedMessageDto insertUserDismissedMessage(UserDto userDto, ProjectDto projectDto, CeTaskMessageType messageType) {
UserDismissedMessageDto dto = new UserDismissedMessageDto()
.setUuid(Uuids.create())
.setUserUuid(userDto.getUuid())
.setProjectUuid(projectDto.getUuid())
.setCeMessageType(messageType);
db.getDbClient().userDismissedMessagesDao().insert(db.getSession(), dto);
db.commit();
return dto;
}

}

+ 42
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTable.java View File

@@ -0,0 +1,42 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.v85;

import java.sql.SQLException;
import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.sql.CreateIndexBuilder;
import org.sonar.server.platform.db.migration.step.DdlChange;

public class AddIndexOnMessageTypeColumnOfCeTaskMessageTable extends DdlChange {
private static final String TABLE = "ce_task_message";

public AddIndexOnMessageTypeColumnOfCeTaskMessageTable(Database db) {
super(db);
}

@Override
public void execute(Context context) throws SQLException {
context.execute(new CreateIndexBuilder()
.setTable(TABLE)
.setName("ctm_message_type")
.addColumn("message_type")
.build());
}
}

server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddIsDismissibleColumnToCeTaskMessageTable.java → server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTable.java View File

@@ -21,29 +21,29 @@ package org.sonar.server.platform.db.migration.version.v85;

import java.sql.SQLException;
import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
import org.sonar.server.platform.db.migration.sql.AddColumnsBuilder;
import org.sonar.server.platform.db.migration.step.DdlChange;

import static org.sonar.server.platform.db.migration.def.BooleanColumnDef.newBooleanColumnDefBuilder;
import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;

public class AddIsDismissibleColumnToCeTaskMessageTable extends DdlChange {
public class AddMessageTypeColumnToCeTaskMessageTable extends DdlChange {
private static final String TABLE = "ce_task_message";
private static final String NEW_COLUMN = "is_dismissible";

private static final BooleanColumnDef IS_DISMISSIBLE = newBooleanColumnDefBuilder()
.setColumnName(NEW_COLUMN)
private static final VarcharColumnDef MESSAGE_TYPE = newVarcharColumnDefBuilder()
.setColumnName("message_type")
.setIsNullable(true)
.setLimit(255)
.build();

public AddIsDismissibleColumnToCeTaskMessageTable(Database db) {
public AddMessageTypeColumnToCeTaskMessageTable(Database db) {
super(db);
}

@Override
public void execute(Context context) throws SQLException {
context.execute(new AddColumnsBuilder(getDialect(), TABLE)
.addColumn(IS_DISMISSIBLE)
.addColumn(MESSAGE_TYPE)
.build());
}
}

+ 102
- 0
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/CreateUserDismissedMessagesTable.java View File

@@ -0,0 +1,102 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.v85;

import java.sql.SQLException;
import org.sonar.db.Database;
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.CreateIndexBuilder;
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;

public class CreateUserDismissedMessagesTable extends DdlChange {

private static final String TABLE_NAME = "user_dismissed_messages";

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(VarcharColumnDef.USER_UUID_SIZE)
.build();

private static final VarcharColumnDef PROJECT_UUID = newVarcharColumnDefBuilder()
.setColumnName("project_uuid")
.setIsNullable(false)
.setLimit(UUID_SIZE)
.build();

private static final VarcharColumnDef MESSAGE_TYPE = newVarcharColumnDefBuilder()
.setColumnName("message_type")
.setIsNullable(false)
.setLimit(255)
.build();

private static final BigIntegerColumnDef CREATED_AT = newBigIntegerColumnDefBuilder()
.setColumnName("created_at")
.setIsNullable(false)
.build();

public CreateUserDismissedMessagesTable(Database db) {
super(db);
}

@Override
public void execute(Context context) throws SQLException {
context.execute(new CreateTableBuilder(getDialect(), TABLE_NAME)
.addPkColumn(UUID_COLUMN)
.addColumn(USER_UUID_COLUMN)
.addColumn(PROJECT_UUID)
.addColumn(MESSAGE_TYPE)
.addColumn(CREATED_AT)
.build());

context.execute(new CreateIndexBuilder()
.setTable(TABLE_NAME)
.setName("uniq_user_dismissed_messages")
.setUnique(true)
.addColumn(USER_UUID_COLUMN)
.addColumn(PROJECT_UUID)
.addColumn(MESSAGE_TYPE)
.build());

context.execute(new CreateIndexBuilder()
.setTable(TABLE_NAME)
.setName("udm_project_uuid")
.addColumn(PROJECT_UUID)
.build());

context.execute(new CreateIndexBuilder()
.setTable(TABLE_NAME)
.setName("udm_message_type")
.addColumn(MESSAGE_TYPE)
.build());
}
}

+ 6
- 4
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/DbVersion85.java View File

@@ -47,9 +47,11 @@ public class DbVersion85 implements DbVersion {
.add(4016, "Add 'type' column to 'plugins' table", AddTypeToPlugins.class)
.add(4017, "Populate 'type' column in 'plugins' table", PopulateTypeInPlugins.class)
.add(4018, "Alter 'type' column in 'plugins' to not nullable", AlterTypeInPluginNotNullable.class)
.add(4019, "Add 'is_dismissible' column to `ce_task_message` table", AddIsDismissibleColumnToCeTaskMessageTable.class)
.add(4020, "Populate 'is_dismissible' column of `ce_task_message` table", PopulateIsDismissibleColumnOfCeTaskMessageTable.class)
.add(4021, "Make 'is_dismissible' column not nullable for `ce_task_message` table", MakeIsDismissibleColumnNotNullableOnCeTaskMessageTable.class)
;
.add(4019, "Add 'message_type' column to 'ce_task_message' table", AddMessageTypeColumnToCeTaskMessageTable.class)
.add(4020, "Populate 'message_type' column of 'ce_task_message' table", PopulateMessageTypeColumnOfCeTaskMessageTable.class)
.add(4021, "Make 'message_type' column not nullable for `ce_task_message` table", MakeMessageTypeColumnNotNullableOnCeTaskMessageTable.class)
.add(4022, "Add index on 'message_type' column of `ce_task_message` table", AddIndexOnMessageTypeColumnOfCeTaskMessageTable.class)
.add(4023, "Create 'user_dismissed_messages' table", CreateUserDismissedMessagesTable.class
);
}
}

server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/MakeIsDismissibleColumnNotNullableOnCeTaskMessageTable.java → server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTable.java View File

@@ -21,30 +21,27 @@ package org.sonar.server.platform.db.migration.version.v85;

import java.sql.SQLException;
import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.def.BooleanColumnDef;
import org.sonar.server.platform.db.migration.def.VarcharColumnDef;
import org.sonar.server.platform.db.migration.sql.AlterColumnsBuilder;
import org.sonar.server.platform.db.migration.step.DdlChange;

import static org.sonar.server.platform.db.migration.def.BooleanColumnDef.newBooleanColumnDefBuilder;
import static org.sonar.server.platform.db.migration.def.VarcharColumnDef.newVarcharColumnDefBuilder;

public class MakeIsDismissibleColumnNotNullableOnCeTaskMessageTable extends DdlChange {

private static final String TABLE = "ce_task_message";
private static final String NEW_COLUMN = "is_dismissible";

private static final BooleanColumnDef IS_DISMISSIBLE = newBooleanColumnDefBuilder()
.setColumnName(NEW_COLUMN)
public class MakeMessageTypeColumnNotNullableOnCeTaskMessageTable extends DdlChange {
private static final VarcharColumnDef MESSAGE_TYPE = newVarcharColumnDefBuilder()
.setColumnName("message_type")
.setIsNullable(false)
.setLimit(255)
.build();

public MakeIsDismissibleColumnNotNullableOnCeTaskMessageTable(Database db) {
public MakeMessageTypeColumnNotNullableOnCeTaskMessageTable(Database db) {
super(db);
}

@Override
public void execute(Context context) throws SQLException {
context.execute(new AlterColumnsBuilder(getDialect(), TABLE)
.updateColumn(IS_DISMISSIBLE)
context.execute(new AlterColumnsBuilder(getDialect(), "ce_task_message")
.updateColumn(MESSAGE_TYPE)
.build());
}
}

server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateIsDismissibleColumnOfCeTaskMessageTable.java → server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTable.java View File

@@ -24,20 +24,18 @@ import org.sonar.db.Database;
import org.sonar.server.platform.db.migration.step.DataChange;
import org.sonar.server.platform.db.migration.step.MassUpdate;

public class PopulateIsDismissibleColumnOfCeTaskMessageTable extends DataChange {

public PopulateIsDismissibleColumnOfCeTaskMessageTable(Database db) {
public class PopulateMessageTypeColumnOfCeTaskMessageTable extends DataChange {
public PopulateMessageTypeColumnOfCeTaskMessageTable(Database db) {
super(db);
}

@Override
protected void execute(Context context) throws SQLException {
protected void execute(DataChange.Context context) throws SQLException {
MassUpdate massUpdate = context.prepareMassUpdate();
massUpdate.select("select ctm.uuid from ce_task_message ctm where ctm.is_dismissible is null");
massUpdate.update("update ce_task_message set is_dismissible = ? where uuid = ?");

massUpdate.select("select ctm.uuid from ce_task_message ctm where ctm.message_type is null");
massUpdate.update("update ce_task_message set message_type = ? where uuid = ?");
massUpdate.execute((row, update) -> {
update.setBoolean(1, false);
update.setString(1, "GENERIC");
update.setString(2, row.getString(1));
return true;
});

server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddIsDismissibleColumnToCeTaskMessageTableTest.java → server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest.java View File

@@ -17,7 +17,6 @@
* 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.v85;

import java.sql.SQLException;
@@ -26,20 +25,19 @@ import org.junit.Test;
import org.sonar.db.CoreDbTester;
import org.sonar.server.platform.db.migration.step.DdlChange;

import static java.sql.Types.BOOLEAN;
public class AddIsDismissibleColumnToCeTaskMessageTableTest {
public class AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest {
private static final String TABLE_NAME = "ce_task_message";
private static final String INDEX_NAME = "ctm_message_type";

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(AddIsDismissibleColumnToCeTaskMessageTableTest.class, "schema.sql");
public CoreDbTester db = CoreDbTester.createForSchema(AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest.class, "schema.sql");

DdlChange underTest = new AddIsDismissibleColumnToCeTaskMessageTable(db.database());
DdlChange underTest = new AddIndexOnMessageTypeColumnOfCeTaskMessageTable(db.database());

@Test
public void add_column() throws SQLException {
public void add_index() throws SQLException {
underTest.execute();

db.assertColumnDefinition("ce_task_message", "is_dismissible", BOOLEAN, null, true);
db.assertIndex(TABLE_NAME, INDEX_NAME, "message_type");
}

}

+ 63
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTableTest.java View File

@@ -0,0 +1,63 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.v85;

import java.sql.SQLException;
import java.sql.Types;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.db.CoreDbTester;
import org.sonar.server.platform.db.migration.step.DdlChange;

import static org.assertj.core.api.Assertions.assertThat;

public class AddMessageTypeColumnToCeTaskMessageTableTest {

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(AddMessageTypeColumnToCeTaskMessageTableTest.class, "schema.sql");

private final DdlChange underTest = new AddMessageTypeColumnToCeTaskMessageTable(db.database());

@Before
public void setup() {
insertTaskMessage(1L, "a1");
insertTaskMessage(2L, "a2");
insertTaskMessage(3L, "a3");
}

@Test
public void add_message_type_column_to_ce_task_message_table() throws SQLException {
underTest.execute();

db.assertColumnDefinition("ce_task_message", "message_type", Types.VARCHAR, 255, true);

assertThat(db.countSql("select count(uuid) from ce_task_message")).isEqualTo(3);
}

private void insertTaskMessage(Long id, String message) {
db.executeInsert("ce_task_message",
"uuid", "uuid-" + id,
"task_uuid", "task-uuid-" + id,
"message", message,
"created_at", System2.INSTANCE.now());
}
}

+ 55
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/CreateUserDismissedMessagesTableTest.java View File

@@ -0,0 +1,55 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.v85;

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 CreateUserDismissedMessagesTableTest {
private static final String TABLE_NAME = "user_dismissed_messages";

@Rule
public CoreDbTester dbTester = CoreDbTester.createEmpty();

private final CreateUserDismissedMessagesTable underTest = new CreateUserDismissedMessagesTable(dbTester.database());

@Test
public void table_has_been_created() throws SQLException {
underTest.execute();

dbTester.assertTableExists(TABLE_NAME);
dbTester.assertPrimaryKey(TABLE_NAME, "pk_user_dismissed_messages", "uuid");
dbTester.assertColumnDefinition(TABLE_NAME, "uuid", VARCHAR, 40, false);
dbTester.assertColumnDefinition(TABLE_NAME, "user_uuid", VARCHAR, 255, false);
dbTester.assertColumnDefinition(TABLE_NAME, "project_uuid", VARCHAR, 40, false);
dbTester.assertColumnDefinition(TABLE_NAME, "message_type", VARCHAR, 255, false);
dbTester.assertColumnDefinition(TABLE_NAME, "created_at", BIGINT, null, false);
dbTester.assertUniqueIndex(TABLE_NAME, "uniq_user_dismissed_messages", "user_uuid",
"project_uuid", "message_type");
dbTester.assertIndex(TABLE_NAME, "udm_project_uuid", "project_uuid");
dbTester.assertIndex(TABLE_NAME, "udm_message_type", "message_type");
}

}

server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/MakeIsDismissibleColumnNotNullableOnCeTaskMessageTableTest.java → server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest.java View File

@@ -25,20 +25,19 @@ import org.junit.Test;
import org.sonar.db.CoreDbTester;
import org.sonar.server.platform.db.migration.step.MigrationStep;

import static java.sql.Types.BOOLEAN;
import static java.sql.Types.VARCHAR;

public class MakeIsDismissibleColumnNotNullableOnCeTaskMessageTableTest {
public class MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest {

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(MakeIsDismissibleColumnNotNullableOnCeTaskMessageTableTest.class, "schema.sql");
public CoreDbTester db = CoreDbTester.createForSchema(MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest.class, "schema.sql");

private MigrationStep underTest = new MakeIsDismissibleColumnNotNullableOnCeTaskMessageTable(db.database());
private final MigrationStep underTest = new MakeMessageTypeColumnNotNullableOnCeTaskMessageTable(db.database());

@Test
public void ce_task_message_column_is_not_null() throws SQLException {
public void message_type_column_is_not_null() throws SQLException {
underTest.execute();

db.assertColumnDefinition("ce_task_message", "is_dismissible", BOOLEAN, null, false);
db.assertColumnDefinition("ce_task_message", "message_type", VARCHAR, 255, false);
}

}

+ 0
- 87
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateIsDismissibleColumnOfCeTaskMessageTableTest.java View File

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

import java.sql.SQLException;
import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.core.util.Uuids;
import org.sonar.db.CoreDbTester;

import static org.assertj.core.api.Assertions.assertThat;

public class PopulateIsDismissibleColumnOfCeTaskMessageTableTest {

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(PopulateIsDismissibleColumnOfCeTaskMessageTableTest.class, "schema.sql");

private PopulateIsDismissibleColumnOfCeTaskMessageTable underTest = new PopulateIsDismissibleColumnOfCeTaskMessageTable(db.database());

@Test
public void execute_migration() throws SQLException {
insertCeTaskMessage(null);
insertCeTaskMessage(null);
insertCeTaskMessage(null);

underTest.execute();

assertIsDismissibleValuesAreAllFalse();
}

@Test
public void migrate_not_already_updated_rows() throws SQLException {
insertCeTaskMessage(false);
insertCeTaskMessage(false);
insertCeTaskMessage(null);

underTest.execute();

assertIsDismissibleValuesAreAllFalse();
}

@Test
public void migration_is_reentrant() throws SQLException {
insertCeTaskMessage(null);

underTest.execute();
underTest.execute();

assertIsDismissibleValuesAreAllFalse();
}

private void assertIsDismissibleValuesAreAllFalse() {
assertThat(db.select("select is_dismissible as \"IS_DISMISSIBLE\" from ce_task_message")
.stream()
.map(rows -> rows.get("IS_DISMISSIBLE")))
.containsOnly(false);
}

private String insertCeTaskMessage(@Nullable Boolean isDissmisible) {
String uuid = Uuids.createFast();
db.executeInsert("CE_TASK_MESSAGE",
"UUID", uuid,
"TASK_UUID", Uuids.createFast(),
"MESSAGE", "message-" + uuid,
"IS_DISMISSIBLE", isDissmisible,
"CREATED_AT", System.currentTimeMillis());
return uuid;
}
}

+ 73
- 0
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTableTest.java View File

@@ -0,0 +1,73 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.v85;

import java.sql.SQLException;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.utils.System2;
import org.sonar.db.CoreDbTester;
import org.sonar.server.platform.db.migration.step.DataChange;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;

public class PopulateMessageTypeColumnOfCeTaskMessageTableTest {

@Rule
public CoreDbTester db = CoreDbTester.createForSchema(PopulateMessageTypeColumnOfCeTaskMessageTableTest.class, "schema.sql");

private final DataChange underTest = new PopulateMessageTypeColumnOfCeTaskMessageTable(db.database());

@Test
public void add_message_type_column_on_empty_table() throws SQLException {
underTest.execute();

assertThat(db.countSql("select count(*) from ce_task_message")).isZero();
}

@Test
public void add_message_type_column_on_non_empty_table() throws SQLException {
insertTaskMessage(1L, "msg1");
insertTaskMessage(2L, "msg2");
insertTaskMessage(3L, "msg3");
insertTaskMessage(4L, "msg4");

underTest.execute();

assertThat(db.countSql("select count(*) from ce_task_message")).isEqualTo(4);
assertThat(db.select("select uuid, task_uuid, message, message_type from ce_task_message"))
.extracting(m -> m.get("UUID"), m -> m.get("TASK_UUID"), m -> m.get("MESSAGE"), m -> m.get("MESSAGE_TYPE"))
.containsExactlyInAnyOrder(
tuple("uuid-1", "task-uuid-1", "msg1", "GENERIC"),
tuple("uuid-2", "task-uuid-2", "msg2", "GENERIC"),
tuple("uuid-3", "task-uuid-3", "msg3", "GENERIC"),
tuple("uuid-4", "task-uuid-4", "msg4", "GENERIC"));
}

private void insertTaskMessage(Long id, String message) {
db.executeInsert("ce_task_message",
"uuid", "uuid-" + id,
"task_uuid", "task-uuid-" + id,
"message", message,
"created_at", System2.INSTANCE.now());
}

}

+ 9
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddIndexOnMessageTypeColumnOfCeTaskMessageTableTest/schema.sql View File

@@ -0,0 +1,9 @@
CREATE TABLE "CE_TASK_MESSAGE"(
"UUID" VARCHAR(40) NOT NULL,
"TASK_UUID" VARCHAR(40) NOT NULL,
"MESSAGE" VARCHAR(4000) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
"MESSAGE_TYPE" VARCHAR(255) NOT NULL
);
ALTER TABLE "CE_TASK_MESSAGE" ADD CONSTRAINT "PK_CE_TASK_MESSAGE" PRIMARY KEY("UUID");
CREATE INDEX "CE_TASK_MESSAGE_TASK" ON "CE_TASK_MESSAGE"("TASK_UUID");

+ 9
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/AddMessageTypeColumnToCeTaskMessageTableTest/schema.sql View File

@@ -0,0 +1,9 @@
CREATE TABLE "CE_TASK_MESSAGE" (
"UUID" VARCHAR(40) NOT NULL,
"TASK_UUID" VARCHAR(40) NOT NULL,
"MESSAGE" VARCHAR(4000) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,

CONSTRAINT "CE_TASK_MESSAGE" PRIMARY KEY ("UUID")
);
CREATE INDEX "CE_TASK_MESSAGE_TASK" ON "CE_TASK_MESSAGE" ("TASK_UUID");

+ 9
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/MakeMessageTypeColumnNotNullableOnCeTaskMessageTableTest/schema.sql View File

@@ -0,0 +1,9 @@
CREATE TABLE "CE_TASK_MESSAGE"(
"UUID" VARCHAR(40) NOT NULL,
"TASK_UUID" VARCHAR(40) NOT NULL,
"MESSAGE" VARCHAR(4000) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
"MESSAGE_TYPE" VARCHAR(255)
);
ALTER TABLE "CE_TASK_MESSAGE" ADD CONSTRAINT "PK_CE_TASK_MESSAGE" PRIMARY KEY("UUID");
CREATE INDEX "CE_TASK_MESSAGE_TASK" ON "CE_TASK_MESSAGE"("TASK_UUID");

+ 9
- 0
server/sonar-db-migration/src/test/resources/org/sonar/server/platform/db/migration/version/v85/PopulateMessageTypeColumnOfCeTaskMessageTableTest/schema.sql View File

@@ -0,0 +1,9 @@
CREATE TABLE "CE_TASK_MESSAGE"(
"UUID" VARCHAR(40) NOT NULL,
"TASK_UUID" VARCHAR(40) NOT NULL,
"MESSAGE" VARCHAR(4000) NOT NULL,
"CREATED_AT" BIGINT NOT NULL,
"MESSAGE_TYPE" VARCHAR(255)
);
ALTER TABLE "CE_TASK_MESSAGE" ADD CONSTRAINT "PK_CE_TASK_MESSAGE" PRIMARY KEY("UUID");
CREATE INDEX "CE_TASK_MESSAGE_TASK" ON "CE_TASK_MESSAGE"("TASK_UUID");

+ 70
- 0
server/sonar-webserver-core/src/main/java/org/sonar/server/startup/UpgradeSuggestionsCleaner.java View File

@@ -0,0 +1,70 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.startup;

import org.picocontainer.Startable;
import org.sonar.api.SonarEdition;
import org.sonar.api.SonarRuntime;
import org.sonar.api.server.ServerSide;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.ce.CeTaskMessageType;

/**
* Clean up messages (like removing upgrade suggestions after an edition upgrade)
*/
@ServerSide
public class UpgradeSuggestionsCleaner implements Startable {

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

private final DbClient dbClient;
private final SonarRuntime sonarRuntime;

public UpgradeSuggestionsCleaner(DbClient dbClient, SonarRuntime sonarRuntime) {
this.dbClient = dbClient;
this.sonarRuntime = sonarRuntime;
}

@Override
public void start() {
if (sonarRuntime.getEdition() == SonarEdition.COMMUNITY) {
return;
}

deleteUpgradeMessageDismissals();
}

private void deleteUpgradeMessageDismissals() {
LOGGER.info("Dismissed messages cleanup");
try (DbSession dbSession = dbClient.openSession(false)) {
dbClient.userDismissedMessagesDao().deleteByType(dbSession, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
dbClient.ceTaskMessageDao().deleteByType(dbSession, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
dbSession.commit();
}
}

@Override
public void stop() {
// nothing to do
}
}

+ 148
- 0
server/sonar-webserver-core/src/test/java/org/sonar/server/startup/UpgradeSuggestionsCleanerTest.java View File

@@ -0,0 +1,148 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.startup;

import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.sonar.api.SonarEdition;
import org.sonar.api.SonarRuntime;
import org.sonar.api.utils.System2;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.user.UserDismissedMessageDto;
import org.sonar.db.user.UserDto;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@RunWith(DataProviderRunner.class)
public class UpgradeSuggestionsCleanerTest {
private static final String TASK_UUID = "b8d564dd-4ceb-4dba-8a3d-5fafa2d72cdf";

@Rule
public DbTester dbTester = DbTester.create(System2.INSTANCE);

private final SonarRuntime sonarRuntime = mock(SonarRuntime.class);
private final UpgradeSuggestionsCleaner underTest = new UpgradeSuggestionsCleaner(dbTester.getDbClient(), sonarRuntime);

private UserDto user;

@Before
public void setup() {
user = dbTester.users().insertUser();
}

@DataProvider
public static Object[][] editionsWithCleanup() {
return new Object[][] {
{SonarEdition.DEVELOPER},
{SonarEdition.ENTERPRISE},
{SonarEdition.DATACENTER}
};
}

@DataProvider
public static Object[][] allEditions() {
return new Object[][] {
{SonarEdition.COMMUNITY},
{SonarEdition.DEVELOPER},
{SonarEdition.ENTERPRISE},
{SonarEdition.DATACENTER}
};
}

@Test
@UseDataProvider("editionsWithCleanup")
public void start_cleans_up_obsolete_upgrade_suggestions(SonarEdition edition) {
when(sonarRuntime.getEdition()).thenReturn(edition);
insertCeTaskMessage("ctm1", CeTaskMessageType.GENERIC, "msg1");
insertCeTaskMessage("ctm2", CeTaskMessageType.GENERIC, "msg2");
insertCeTaskMessage("ctm3", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE, "upgrade-msg-1");
insertInUserDismissedMessages("u1", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
insertInUserDismissedMessages("u2", CeTaskMessageType.GENERIC);

underTest.start();
underTest.stop();

assertThat(dbTester.getDbClient().ceTaskMessageDao().selectByTask(dbTester.getSession(), TASK_UUID))
.extracting(CeTaskMessageDto::getUuid)
.containsExactly("ctm1", "ctm2");
assertThat(dbTester.getDbClient().userDismissedMessagesDao().selectByUser(dbTester.getSession(), user))
.extracting(UserDismissedMessageDto::getUuid)
.containsExactly("u2");
}

@Test
public void start_does_nothing_in_community_edition() {
when(sonarRuntime.getEdition()).thenReturn(SonarEdition.COMMUNITY);
insertCeTaskMessage("ctm1", CeTaskMessageType.GENERIC, "msg1");
insertCeTaskMessage("ctm2", CeTaskMessageType.GENERIC, "msg2");
insertCeTaskMessage("ctm3", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE, "upgrade-msg-1");
insertInUserDismissedMessages("u1", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
insertInUserDismissedMessages("u2", CeTaskMessageType.GENERIC);

underTest.start();

assertThat(dbTester.getDbClient().ceTaskMessageDao().selectByTask(dbTester.getSession(), TASK_UUID))
.extracting(CeTaskMessageDto::getUuid)
.containsExactly("ctm1", "ctm2", "ctm3");
assertThat(dbTester.getDbClient().userDismissedMessagesDao().selectByUser(dbTester.getSession(), user))
.extracting(UserDismissedMessageDto::getUuid)
.containsExactlyInAnyOrder("u1", "u2");
}

@Test
@UseDataProvider("allEditions")
public void start_does_nothing_when_no_suggest_upgrade_messages(SonarEdition edition) {
when(sonarRuntime.getEdition()).thenReturn(edition);

underTest.start();

assertThat(dbTester.getDbClient().ceTaskMessageDao().selectByTask(dbTester.getSession(), TASK_UUID)).isEmpty();
assertThat(dbTester.getDbClient().userDismissedMessagesDao().selectByUser(dbTester.getSession(), user)).isEmpty();
}

private void insertCeTaskMessage(String uuid, CeTaskMessageType messageType, String msg) {
CeTaskMessageDto dto = new CeTaskMessageDto()
.setUuid(uuid)
.setMessage(msg)
.setType(messageType)
.setTaskUuid(TASK_UUID);
dbTester.getDbClient().ceTaskMessageDao().insert(dbTester.getSession(), dto);
dbTester.getSession().commit();
}

private void insertInUserDismissedMessages(String uuid, CeTaskMessageType messageType) {
UserDismissedMessageDto dto = new UserDismissedMessageDto()
.setUuid(uuid)
.setUserUuid(user.getUuid())
.setProjectUuid("PROJECT_1")
.setCeMessageType(messageType);
dbTester.getDbClient().userDismissedMessagesDao().insert(dbTester.getSession(), dto);
dbTester.getSession().commit();
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/AnalysisStatusAction.java View File

@@ -128,7 +128,7 @@ public class AnalysisStatusAction implements CeWsAction {
.map(dto -> AnalysisStatusWsResponse.Warning.newBuilder()
.setKey(dto.getUuid())
.setMessage(dto.getMessage())
.setDismissable(dto.isDismissible())
.setDismissable(dto.getType().isDismissible())
.build())
.collect(Collectors.toList());


+ 2
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/CeWsModule.java View File

@@ -46,6 +46,7 @@ public class CeWsModule extends Module {
TaskFormatter.class,
TaskAction.class,
TaskTypesAction.class,
WorkerCountAction.class);
WorkerCountAction.class,
DismissAnalysisWarningAction.class);
}
}

+ 115
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/ce/ws/DismissAnalysisWarningAction.java View File

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

import java.util.Optional;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.web.UserRole;
import org.sonar.core.util.Uuids;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDismissedMessageDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.exceptions.NotFoundException;
import org.sonar.server.user.UserSession;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;
import static org.sonar.server.exceptions.NotFoundException.checkFound;

public class DismissAnalysisWarningAction implements CeWsAction {

private static final String MESSAGE_NOT_FOUND = "Message '%s' not found.";
private static final String MESSAGE_CANNOT_BE_DISMISSED = "Message '%s' cannot be dismissed.";
private static final String PARAM_COMPONENT_KEY = "component";
private static final String PARAM_MESSAGE_KEY = "warning";

private final UserSession userSession;
private final DbClient dbClient;
private final ComponentFinder componentFinder;

public DismissAnalysisWarningAction(UserSession userSession, DbClient dbClient, ComponentFinder componentFinder) {
this.userSession = userSession;
this.dbClient = dbClient;
this.componentFinder = componentFinder;
}

@Override
public void define(WebService.NewController controller) {
WebService.NewAction action = controller
.createAction("dismiss_analysis_warning")
.setPost(true)
.setDescription("Permanently dismiss a specific analysis warning. Requires authentication and 'Browse' permission on the specified project.")
.setSince("8.5")
.setInternal(true)
.setHandler(this);

action.createParam(PARAM_COMPONENT_KEY)
.setDescription("Key of the project")
.setRequired(true)
.setExampleValue(Uuids.UUID_EXAMPLE_01);
action.createParam(PARAM_MESSAGE_KEY)
.setDescription("Key of the warning")
.setRequired(true)
.setExampleValue(Uuids.UUID_EXAMPLE_02);
}

@Override
public void handle(Request request, Response response) throws Exception {
userSession.checkLoggedIn();
String userLogin = requireNonNull(userSession.getLogin());
String projectKey = request.mandatoryParam(PARAM_COMPONENT_KEY);
String messageKey = request.mandatoryParam(PARAM_MESSAGE_KEY);

try (DbSession dbSession = dbClient.openSession(false)) {
UserDto user = getUser(dbSession, userLogin);
ProjectDto project = componentFinder.getProjectByKey(dbSession, projectKey);
userSession.checkProjectPermission(UserRole.USER, project);

CeTaskMessageDto messageDto = dbClient.ceTaskMessageDao()
.selectByUuid(dbSession, messageKey)
.orElseThrow(() -> new NotFoundException(format(MESSAGE_NOT_FOUND, messageKey)));
if (!messageDto.getType().isDismissible()) {
throw new IllegalArgumentException(format(MESSAGE_CANNOT_BE_DISMISSED, messageKey));
}

Optional<UserDismissedMessageDto> result = dbClient.userDismissedMessagesDao().selectByUserAndProjectAndMessageType(dbSession, user, project, messageDto.getType());
if (!result.isPresent()) {
dbClient.userDismissedMessagesDao().insert(dbSession, new UserDismissedMessageDto()
.setUuid(Uuids.create())
.setUserUuid(user.getUuid())
.setProjectUuid(project.getUuid())
.setCeMessageType(messageDto.getType()));
dbSession.commit();
}

response.noContent();
}
}

private UserDto getUser(DbSession dbSession, String userLogin) {
return checkFound(dbClient.userDao().selectByLogin(dbSession, userLogin), "User '%s' not found", userLogin);
}
}

+ 1
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/user/ws/DeactivateAction.java View File

@@ -27,8 +27,6 @@ import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.server.ws.WebService.NewAction;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.api.utils.text.JsonWriter;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
@@ -49,8 +47,6 @@ import static org.sonar.server.exceptions.NotFoundException.checkFound;

public class DeactivateAction implements UsersWsAction {

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

private static final String PARAM_LOGIN = "login";

private final DbClient dbClient;
@@ -109,6 +105,7 @@ public class DeactivateAction implements UsersWsAction {
dbClient.userPropertiesDao().deleteByUser(dbSession, user);
dbClient.almPatDao().deleteByUser(dbSession, user);
dbClient.sessionTokensDao().deleteByUser(dbSession, user);
dbClient.userDismissedMessagesDao().deleteByUser(dbSession, user);
dbClient.userDao().deactivateUser(dbSession, user);
userIndexer.commitAndIndex(dbSession, user);
}

+ 2
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/ActivityActionTest.java View File

@@ -40,6 +40,7 @@ import org.sonar.db.ce.CeActivityDto.Status;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskCharacteristicDto;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
@@ -247,6 +248,7 @@ public class ActivityActionTest {
.setUuid("uuid_" + taskUuid + "_" + i)
.setTaskUuid(taskUuid)
.setMessage("m_" + taskUuid + "_" + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(taskUuid.hashCode() + i)));
db.commit();
}

+ 6
- 5
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/AnalysisStatusActionTest.java View File

@@ -30,6 +30,7 @@ import org.sonar.db.DbTester;
import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.BranchType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
@@ -92,7 +93,7 @@ public class AnalysisStatusActionTest {
SnapshotDto analysis = db.components().insertSnapshot(project);
CeActivityDto activity = insertActivity("task-uuid" + counter++, project, SUCCESS, analysis, REPORT);
CeTaskMessageDto taskMessage = createTaskMessage(activity, WARNING_IN_MAIN);
CeTaskMessageDto taskMessageDismissible = createTaskMessage(activity, "Dismissible warning", true);
CeTaskMessageDto taskMessageDismissible = createTaskMessage(activity, "Dismissible warning", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

Ce.AnalysisStatusWsResponse response = ws.newRequest()
.setParam(PARAM_COMPONENT, project.getKey())
@@ -288,7 +289,7 @@ public class AnalysisStatusActionTest {
.setUuid("AU-Tpxb--iU5OvuD2FLy")
.setTaskUuid(activity.getUuid())
.setMessage("Property \"sonar.jacoco.reportPaths\" is no longer supported. Use JaCoCo xml report and sonar-jacoco plugin.")
.setDismissible(false)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(counter);
db.getDbClient().ceTaskMessageDao().insert(db.getSession(), ceTaskMessage);
db.commit();
@@ -329,15 +330,15 @@ public class AnalysisStatusActionTest {
}

private CeTaskMessageDto createTaskMessage(CeActivityDto activity, String warning) {
return createTaskMessage(activity, warning, false);
return createTaskMessage(activity, warning, CeTaskMessageType.GENERIC);
}

private CeTaskMessageDto createTaskMessage(CeActivityDto activity, String warning, boolean dismissible) {
private CeTaskMessageDto createTaskMessage(CeActivityDto activity, String warning, CeTaskMessageType messageType) {
CeTaskMessageDto ceTaskMessageDto = new CeTaskMessageDto()
.setUuid("m-uuid-" + counter++)
.setTaskUuid(activity.getUuid())
.setMessage(warning)
.setDismissible(dismissible)
.setType(messageType)
.setCreatedAt(counter);
db.getDbClient().ceTaskMessageDao().insert(db.getSession(), ceTaskMessageDto);
db.commit();

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/CeWsModuleTest.java View File

@@ -31,6 +31,6 @@ public class CeWsModuleTest {
public void verify_count_of_added_components() {
ComponentContainer container = new ComponentContainer();
new CeWsModule().configure(container);
assertThat(container.size()).isEqualTo(19 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
assertThat(container.size()).isEqualTo(20 + COMPONENTS_IN_EMPTY_COMPONENT_CONTAINER);
}
}

+ 2
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/ComponentActionTest.java View File

@@ -34,6 +34,7 @@ import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskCharacteristicDto;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
@@ -255,6 +256,7 @@ public class ComponentActionTest {
.setUuid("uuid_" + i)
.setTaskUuid(activity.getUuid())
.setMessage("m_" + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(i)));
db.commit();


+ 208
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/DismissAnalysisWarningActionTest.java View File

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

import javax.annotation.Nullable;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbTester;
import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.component.SnapshotDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.component.TestComponentFinder;
import org.sonar.server.exceptions.ForbiddenException;
import org.sonar.server.exceptions.NotFoundException;
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 java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
import static org.junit.Assert.assertThrows;
import static org.sonar.db.ce.CeActivityDto.Status.SUCCESS;
import static org.sonar.db.ce.CeTaskTypes.REPORT;

public class DismissAnalysisWarningActionTest {
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

@Rule
public DbTester db = DbTester.create(System2.INSTANCE);

private static int counter = 1;

private final WsActionTester underTest = new WsActionTester(new DismissAnalysisWarningAction(userSession, db.getDbClient(), TestComponentFinder.from(db)));

@Test
public void definition() {
WebService.Action def = underTest.getDef();
assertThat(def.key()).isEqualTo("dismiss_analysis_warning");
assertThat(def.isInternal()).isTrue();
assertThat(def.isPost()).isTrue();
assertThat(def.params()).extracting(WebService.Param::key, WebService.Param::isRequired).containsOnly(
tuple("component", true),
tuple("warning", true));
}

@Test
public void return_401_if_user_is_not_logged_in() {
userSession.anonymous();
TestRequest request = underTest.newRequest()
.setParam("component", "6653f062-7c03-4b55-bcd2-0dac67640c4d")
.setParam("warning", "55c40b35-4145-4b78-bdf2-dfb242c25f15");

assertThrows("Authentication is required", UnauthorizedException.class, request::execute);
}

@Test
public void return_403_if_user_has_no_browse_permission_on_private_project() {
ProjectDto project = db.components().insertPrivateProjectDto();
UserDto user = db.users().insertUser();
userSession.logIn(user);

TestRequest request = underTest.newRequest()
.setParam("component", project.getKee())
.setParam("warning", "55c40b35-4145-4b78-bdf2-dfb242c25f15");

assertThrows("Insufficient privileges", ForbiddenException.class, request::execute);
}

@Test
public void return_204_on_success() {
UserDto user = db.users().insertUser();
ComponentDto project = db.components().insertPrivateProject();
userSession.logIn(user).addProjectPermission(UserRole.USER, project);
SnapshotDto analysis = db.components().insertSnapshot(project);
CeActivityDto activity = insertActivity("task-uuid" + counter++, project, SUCCESS, analysis, REPORT);
CeTaskMessageDto taskMessageDismissible = createTaskMessage(activity, "dismissable warning", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

TestResponse response = underTest.newRequest()
.setParam("component", project.getKey())
.setParam("warning", taskMessageDismissible.getUuid())
.execute();

assertThat(response.getStatus()).isEqualTo(204);
assertThat(db.select("select * from user_dismissed_messages"))
.extracting("USER_UUID", "PROJECT_UUID", "MESSAGE_TYPE")
.containsExactly(tuple(userSession.getUuid(), project.uuid(), CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE.name()));
}

@Test
public void is_idempotent() {
UserDto user = db.users().insertUser();
ComponentDto project = db.components().insertPrivateProject();
userSession.logIn(user).addProjectPermission(UserRole.USER, project);
SnapshotDto analysis = db.components().insertSnapshot(project);
CeActivityDto activity = insertActivity("task-uuid" + counter++, project, SUCCESS, analysis, REPORT);
CeTaskMessageDto taskMessageDismissible = createTaskMessage(activity, "dismissable warning", CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

underTest.newRequest()
.setParam("component", project.getKey())
.setParam("warning", taskMessageDismissible.getUuid())
.execute();
TestResponse response = underTest.newRequest()
.setParam("component", project.getKey())
.setParam("warning", taskMessageDismissible.getUuid())
.execute();

assertThat(response.getStatus()).isEqualTo(204);
assertThat(db.select("select * from user_dismissed_messages"))
.extracting("USER_UUID", "PROJECT_UUID", "MESSAGE_TYPE")
.containsExactly(tuple(userSession.getUuid(), project.uuid(), CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE.name()));
}

@Test
public void returns_400_if_warning_is_not_dismissable() {
UserDto user = db.users().insertUser();
ComponentDto project = db.components().insertPrivateProject();
userSession.logIn(user).addProjectPermission(UserRole.USER, project);
SnapshotDto analysis = db.components().insertSnapshot(project);
CeActivityDto activity = insertActivity("task-uuid" + counter++, project, SUCCESS, analysis, REPORT);
CeTaskMessageDto taskMessage = createTaskMessage(activity, "generic warning");

TestRequest request = underTest.newRequest()
.setParam("component", project.getKey())
.setParam("warning", taskMessage.getUuid());

assertThrows(format("Message '%s' cannot be dismissed.", taskMessage.getUuid()), IllegalArgumentException.class, request::execute);
assertThat(db.countRowsOfTable("USER_DISMISSED_MESSAGES")).isZero();
}

@Test
public void returns_404_if_warning_does_not_exist() {
UserDto user = db.users().insertUser();
ComponentDto project = db.components().insertPrivateProject();
userSession.logIn(user).addProjectPermission(UserRole.USER, project);
SnapshotDto analysis = db.components().insertSnapshot(project);
insertActivity("task-uuid" + counter++, project, SUCCESS, analysis, REPORT);
String warningUuid = "78d1e2ff-3e67-4037-ba58-0d57d5f88e44";

TestRequest request = underTest.newRequest()
.setParam("component", project.getKey())
.setParam("warning", warningUuid);

assertThrows(format("Message '%s' not found", warningUuid), NotFoundException.class, request::execute);
assertThat(db.countRowsOfTable("USER_DISMISSED_MESSAGES")).isZero();
}

private CeTaskMessageDto createTaskMessage(CeActivityDto activity, String warning) {
return createTaskMessage(activity, warning, CeTaskMessageType.GENERIC);
}

private CeTaskMessageDto createTaskMessage(CeActivityDto activity, String warning, CeTaskMessageType messageType) {
CeTaskMessageDto ceTaskMessageDto = new CeTaskMessageDto()
.setUuid("m-uuid-" + counter++)
.setTaskUuid(activity.getUuid())
.setMessage(warning)
.setType(messageType)
.setCreatedAt(counter);
db.getDbClient().ceTaskMessageDao().insert(db.getSession(), ceTaskMessageDto);
db.commit();
return ceTaskMessageDto;
}

private CeActivityDto insertActivity(String taskUuid, ComponentDto component, CeActivityDto.Status status, @Nullable SnapshotDto analysis, String taskType) {
CeQueueDto queueDto = new CeQueueDto();
queueDto.setTaskType(taskType);
queueDto.setComponent(component);
queueDto.setUuid(taskUuid);
CeActivityDto activityDto = new CeActivityDto(queueDto);
activityDto.setStatus(status);
activityDto.setExecutionTimeMs(500L);
activityDto.setAnalysisUuid(analysis == null ? null : analysis.getUuid());
activityDto.setExecutedAt((long) counter++);
activityDto.setTaskType(taskType);
activityDto.setComponentUuid(component.uuid());
db.getDbClient().ceActivityDao().insert(db.getSession(), activityDto);
db.getSession().commit();
return activityDto;
}
}

+ 7
- 4
server/sonar-webserver-webapi/src/test/java/org/sonar/server/ce/ws/TaskActionTest.java View File

@@ -38,6 +38,7 @@ import org.sonar.db.ce.CeActivityDto;
import org.sonar.db.ce.CeQueueDto;
import org.sonar.db.ce.CeTaskCharacteristicDto;
import org.sonar.db.ce.CeTaskMessageDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.ce.CeTaskTypes;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
@@ -74,9 +75,9 @@ public class TaskActionTest {
private OrganizationDto organization;
private ComponentDto privateProject;
private ComponentDto publicProject;
private TaskFormatter formatter = new TaskFormatter(db.getDbClient(), System2.INSTANCE);
private TaskAction underTest = new TaskAction(db.getDbClient(), formatter, userSession);
private WsActionTester ws = new WsActionTester(underTest);
private final TaskFormatter formatter = new TaskFormatter(db.getDbClient(), System2.INSTANCE);
private final TaskAction underTest = new TaskAction(db.getDbClient(), formatter, userSession);
private final WsActionTester ws = new WsActionTester(underTest);

@Before
public void setUp() {
@@ -125,6 +126,7 @@ public class TaskActionTest {
.setUuid("u_" + i)
.setTaskUuid(queueDto.getUuid())
.setMessage("m_" + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(queueDto.getUuid().hashCode() + i)));
db.commit();

@@ -393,7 +395,7 @@ public class TaskActionTest {
CeActivityDto task = createAndPersistArchivedTask(publicProject);

expectedException.expect(ForbiddenException.class);
call(task.getUuid());
}

@@ -488,6 +490,7 @@ public class TaskActionTest {
.setUuid(UuidFactoryFast.getInstance().create())
.setTaskUuid(task.getUuid())
.setMessage("msg_" + task.getUuid() + "_" + i)
.setType(CeTaskMessageType.GENERIC)
.setCreatedAt(task.getUuid().hashCode() + i);
db.getDbClient().ceTaskMessageDao().insert(db.getSession(), res);
db.getSession().commit();

+ 32
- 8
server/sonar-webserver-webapi/src/test/java/org/sonar/server/user/ws/DeactivateActionTest.java View File

@@ -30,15 +30,18 @@ import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.alm.setting.AlmSettingDto;
import org.sonar.db.ce.CeTaskMessageType;
import org.sonar.db.component.ComponentDto;
import org.sonar.db.organization.OrganizationDto;
import org.sonar.db.permission.template.PermissionTemplateDto;
import org.sonar.db.permission.template.PermissionTemplateUserDto;
import org.sonar.db.project.ProjectDto;
import org.sonar.db.property.PropertyDto;
import org.sonar.db.property.PropertyQuery;
import org.sonar.db.qualityprofile.QProfileDto;
import org.sonar.db.user.GroupDto;
import org.sonar.db.user.SessionTokenDto;
import org.sonar.db.user.UserDismissedMessageDto;
import org.sonar.db.user.UserDto;
import org.sonar.server.es.EsTester;
import org.sonar.server.exceptions.BadRequestException;
@@ -71,7 +74,7 @@ import static org.sonar.test.JsonAssert.assertJson;

public class DeactivateActionTest {

private System2 system2 = new AlwaysIncreasingSystem2();
private final System2 system2 = new AlwaysIncreasingSystem2();

@Rule
public ExpectedException expectedException = ExpectedException.none();
@@ -85,11 +88,11 @@ public class DeactivateActionTest {
@Rule
public UserSessionRule userSession = UserSessionRule.standalone();

private DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
private DbClient dbClient = db.getDbClient();
private UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
private DbSession dbSession = db.getSession();
private WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession,
private final DefaultOrganizationProvider defaultOrganizationProvider = TestDefaultOrganizationProvider.from(db);
private final DbClient dbClient = db.getDbClient();
private final UserIndexer userIndexer = new UserIndexer(dbClient, es.client());
private final DbSession dbSession = db.getSession();
private final WsActionTester ws = new WsActionTester(new DeactivateAction(dbClient, userIndexer, userSession,
new UserJsonWriter(userSession), defaultOrganizationProvider));

@Test
@@ -265,9 +268,9 @@ public class DeactivateActionTest {
logInAsSystemAdministrator();
UserDto user = db.users().insertUser();
SessionTokenDto sessionToken1 = db.users().insertSessionToken(user);
SessionTokenDto sessionToken2 =db.users().insertSessionToken(user);
SessionTokenDto sessionToken2 = db.users().insertSessionToken(user);
UserDto anotherUser = db.users().insertUser();
SessionTokenDto sessionToken3 =db.users().insertSessionToken(anotherUser);
SessionTokenDto sessionToken3 = db.users().insertSessionToken(anotherUser);

deactivate(user.getLogin());

@@ -276,6 +279,27 @@ public class DeactivateActionTest {
assertThat(db.getDbClient().sessionTokensDao().selectByUuid(dbSession, sessionToken3.getUuid())).isPresent();
}

@Test
public void deactivate_user_deletes_his_dismissed_messages() {
logInAsSystemAdministrator();
ProjectDto project1 = db.components().insertPrivateProjectDto();
ProjectDto project2 = db.components().insertPrivateProjectDto();
UserDto user = db.users().insertUser();

db.users().insertUserDismissedMessage(user, project1, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
db.users().insertUserDismissedMessage(user, project2, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
UserDto anotherUser = db.users().insertUser();
UserDismissedMessageDto msg3 = db.users().insertUserDismissedMessage(anotherUser, project1, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);
UserDismissedMessageDto msg4 = db.users().insertUserDismissedMessage(anotherUser, project2, CeTaskMessageType.SUGGEST_DEVELOPER_EDITION_UPGRADE);

deactivate(user.getLogin());

assertThat(db.getDbClient().userDismissedMessagesDao().selectByUser(dbSession, user)).isEmpty();
assertThat(db.getDbClient().userDismissedMessagesDao().selectByUser(dbSession, anotherUser))
.extracting(UserDismissedMessageDto::getUuid)
.containsExactlyInAnyOrder(msg3.getUuid(), msg4.getUuid());
}

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

+ 3
- 1
server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelStartup.java View File

@@ -44,6 +44,7 @@ import org.sonar.server.startup.RegisterPlugins;
import org.sonar.server.startup.RenameDeprecatedPropertyKeys;
import org.sonar.server.user.DoPrivileged;
import org.sonar.server.user.ThreadLocalUserSession;
import org.sonar.server.startup.UpgradeSuggestionsCleaner;

public class PlatformLevelStartup extends PlatformLevel {
public PlatformLevelStartup(PlatformLevel parent) {
@@ -70,7 +71,8 @@ public class PlatformLevelStartup extends PlatformLevel {
RegisterQualityProfiles.class,
RegisterPermissionTemplates.class,
RenameDeprecatedPropertyKeys.class,
CeQueueCleaner.class);
CeQueueCleaner.class,
UpgradeSuggestionsCleaner.class);

// RegisterServletFilters makes the WebService engine of Level4 served by the MasterServletFilter, therefor it
// must be started after all the other startup tasks

+ 16
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/ce/CeService.java View File

@@ -141,6 +141,22 @@ public class CeService extends BaseService {
ComponentResponse.parser());
}

/**
*
* This is part of the internal API.
* This is a POST request.
* @see <a href="https://next.sonarqube.com/sonarqube/web_api/api/ce/dismiss_analysis_warning">Further information about this action online (including a response example)</a>
* @since 8.5
*/
public void dismissAnalysisWarning(DismissAnalysisWarningRequest request) {
call(
new PostRequest(path("dismiss_analysis_warning"))
.setParam("component", request.getComponent())
.setParam("warning", request.getWarning())
.setMediaType(MediaTypes.JSON)
).content();
}

/**
*
* This is part of the internal API.

+ 59
- 0
sonar-ws/src/main/java/org/sonarqube/ws/client/ce/DismissAnalysisWarningRequest.java View File

@@ -0,0 +1,59 @@
/*
* SonarQube
* Copyright (C) 2009-2020 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.ce;

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/ce/dismiss_analysis_warning">Further information about this action online (including a response example)</a>
* @since 8.5
*/
@Generated("sonar-ws-generator")
public class DismissAnalysisWarningRequest {

private String component;
private String warning;

/**
* Example value: "AU-Tpxb--iU5OvuD2FLy"
*/
public DismissAnalysisWarningRequest setComponent(String component) {
this.component = component;
return this;
}

public String getComponent() {
return component;
}

/**
* Example value: "AU-TpxcA-iU5OvuD2FLz"
*/
public DismissAnalysisWarningRequest setWarning(String warning) {
this.warning = warning;
return this;
}

public String getWarning() {
return warning;
}
}

Loading…
Cancel
Save