aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLéo Geoffroy <leo.geoffroy@sonarsource.com>2024-11-20 17:09:49 +0100
committersonartech <sonartech@sonarsource.com>2024-11-26 20:02:49 +0000
commit8d96098e30837f5fb8ee7b614dfe39b9486bef26 (patch)
tree2b306e1752babe076362fda8c3b98857259b5779
parentd8d9dcff6d1f21b36758a5d543c1f44b7d6e9d42 (diff)
downloadsonarqube-8d96098e30837f5fb8ee7b614dfe39b9486bef26.tar.gz
sonarqube-8d96098e30837f5fb8ee7b614dfe39b9486bef26.zip
SONAR-23367 Send notification to instance admins about the existence of two modes.
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java5
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractModeNotification.java59
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotification.java30
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandler.java61
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplate.java51
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsModule.java39
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsSender.java84
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotification.java29
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandler.java62
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplate.java58
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandlerTest.java71
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationTest.java42
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplateTest.java70
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsModuleTest.java35
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsSenderTest.java160
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandlerTest.java71
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationTest.java42
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplateTest.java66
-rw-r--r--server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java8
19 files changed, 1040 insertions, 3 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java
index cc7802e943a..613d3585503 100644
--- a/server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java
+++ b/server/sonar-db-dao/src/main/java/org/sonar/db/permission/AuthorizationDao.java
@@ -30,6 +30,7 @@ import org.sonar.db.EmailSubscriberDto;
import static org.sonar.db.DatabaseUtils.executeLargeInputs;
import static org.sonar.db.DatabaseUtils.executeLargeInputsIntoSet;
import static org.sonar.db.permission.GlobalPermission.ADMINISTER;
+import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES;
import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
/**
@@ -152,6 +153,10 @@ public class AuthorizationDao implements Dao {
return mapper(dbSession).selectEmailSubscribersWithGlobalPermission(ADMINISTER_QUALITY_PROFILES.getKey());
}
+ public Set<EmailSubscriberDto> selectQualityGateAdministratorLogins(DbSession dbSession) {
+ return mapper(dbSession).selectEmailSubscribersWithGlobalPermission(ADMINISTER_QUALITY_GATES.getKey());
+ }
+
public Set<EmailSubscriberDto> selectGlobalAdministerEmailSubscribers(DbSession dbSession) {
return mapper(dbSession).selectEmailSubscribersWithGlobalPermission(ADMINISTER.getKey());
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractModeNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractModeNotification.java
new file mode 100644
index 00000000000..a351049bae1
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/AbstractModeNotification.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.sonar.api.notifications.Notification;
+
+public abstract class AbstractModeNotification extends Notification {
+ private final boolean isMQRModeEnabled;
+
+ protected AbstractModeNotification(String type, boolean isMQRModeEnabled) {
+ super(type);
+ this.isMQRModeEnabled = isMQRModeEnabled;
+ }
+
+ public boolean isMQRModeEnabled() {
+ return isMQRModeEnabled;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ AbstractModeNotification that = (AbstractModeNotification) o;
+ return isMQRModeEnabled == that.isMQRModeEnabled;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + Boolean.hashCode(isMQRModeEnabled);
+ return result;
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotification.java
new file mode 100644
index 00000000000..696b7f9d750
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotification.java
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+public class MQRAndStandardModesExistNotification extends AbstractModeNotification {
+
+ public static final String TYPE = "modes-exist";
+
+ public MQRAndStandardModesExistNotification(boolean isMQRModeEnabled) {
+ super(TYPE, isMQRModeEnabled);
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandler.java
new file mode 100644
index 00000000000..404a8f96480
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandler.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.notification.EmailNotificationHandler;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+import static java.util.stream.Collectors.toSet;
+
+public class MQRAndStandardModesExistNotificationHandler extends EmailNotificationHandler<MQRAndStandardModesExistNotification> {
+
+ private final DbClient dbClient;
+
+ protected MQRAndStandardModesExistNotificationHandler(DbClient dbClient, EmailNotificationChannel emailChannel) {
+ super(emailChannel);
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ protected Set<EmailNotificationChannel.EmailDeliveryRequest> toEmailDeliveryRequests(Collection<MQRAndStandardModesExistNotification> notifications) {
+ try (DbSession session = dbClient.openSession(false)) {
+ return dbClient.authorizationDao().selectGlobalAdministerEmailSubscribers(session)
+ .stream()
+ .flatMap(t -> notifications.stream().map(notification -> new EmailNotificationChannel.EmailDeliveryRequest(t.getEmail(), notification)))
+ .collect(toSet());
+ }
+ }
+
+ @Override
+ public Optional<NotificationDispatcherMetadata> getMetadata() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Class<MQRAndStandardModesExistNotification> getNotificationClass() {
+ return MQRAndStandardModesExistNotification.class;
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplate.java
new file mode 100644
index 00000000000..c4b08c8b9d6
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplate.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.jetbrains.annotations.Nullable;
+import org.sonar.api.notifications.Notification;
+
+public class MQRAndStandardModesExistTemplate implements EmailTemplate {
+ @Nullable
+ @Override
+ public EmailMessage format(Notification notification) {
+ if (!MQRAndStandardModesExistNotification.TYPE.equals(notification.getType())) {
+ return null;
+ }
+
+ boolean isMQREnabled = ((MQRAndStandardModesExistNotification) notification).isMQRModeEnabled();
+ String message = """
+ In this version of SonarQube Server, there are two options to reflect the health of all the projects: Multi-Quality Rule (MQR) Mode and Standard Experience.
+ The SonarQube Server documentation explains more.
+
+ Your instance is currently using the %s.
+
+ To change it, go to Administration > Configuration > General Settings > Mode.
+ """
+ .formatted(isMQREnabled ? "Multi-Quality Rule (MQR) Mode" : "Standard Experience");
+
+ // And finally return the email that will be sent
+ return new EmailMessage()
+ .setMessageId(MQRAndStandardModesExistNotification.TYPE)
+ .setSubject("Your SonarQube Server instance is in %s"
+ .formatted(isMQREnabled ? "Multi-Quality Rule (MQR) Mode" : "Standard Experience"))
+ .setPlainTextMessage(message);
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsModule.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsModule.java
new file mode 100644
index 00000000000..9c830e03af0
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsModule.java
@@ -0,0 +1,39 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.sonar.core.platform.Module;
+
+public class NewModesNotificationsModule extends Module {
+ @Override
+ protected void configureModule() {
+ add(
+ NewModesNotificationsSender.class,
+
+ MQRAndStandardModesExistNotification.class,
+ MQRAndStandardModesExistNotificationHandler.class,
+ MQRAndStandardModesExistTemplate.class,
+
+ QualityGateMetricsUpdateNotification.class,
+ QualityGateMetricsUpdateNotificationHandler.class,
+ QualityGateMetricsUpdateTemplate.class);
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsSender.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsSender.java
new file mode 100644
index 00000000000..6fc280561ed
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/NewModesNotificationsSender.java
@@ -0,0 +1,84 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.sonar.api.Startable;
+import org.sonar.api.config.Configuration;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.server.metric.StandardToMQRMetrics;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.platform.db.migration.history.MigrationHistory;
+
+import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_DEFAULT_VALUE;
+import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_ENABLED;
+
+public class NewModesNotificationsSender implements Startable {
+
+ public static final int NEW_MODES_SQ_VERSION = 108_000;
+ private final NotificationManager notificationManager;
+ private final Configuration configuration;
+ private final MigrationHistory migrationHistory;
+ private final DbClient dbClient;
+
+ public NewModesNotificationsSender(NotificationManager notificationManager, Configuration configuration, MigrationHistory migrationHistory, DbClient dbClient) {
+ this.notificationManager = notificationManager;
+ this.configuration = configuration;
+ this.migrationHistory = migrationHistory;
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void start() {
+ if (migrationHistory.getInitialDbVersion() != -1 && migrationHistory.getInitialDbVersion() < NEW_MODES_SQ_VERSION) {
+ boolean isMQRModeEnabled = configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED).orElse(MULTI_QUALITY_MODE_DEFAULT_VALUE);
+ sendNewModesNotification(isMQRModeEnabled);
+ sendQualityGateMetricsUpdateNotification(isMQRModeEnabled);
+ }
+ }
+
+ private void sendQualityGateMetricsUpdateNotification(boolean isMQRModeEnabled) {
+ try (DbSession dbSession = dbClient.openSession(false)) {
+
+ Map<String, String> metricKeysByUuids = dbClient.metricDao().selectAll(dbSession).stream()
+ .collect(Collectors.toMap(MetricDto::getUuid, MetricDto::getKey));
+
+ boolean hasConditionsFromOtherMode = dbClient.gateConditionDao().selectAll(dbSession).stream()
+ .anyMatch(c -> isMQRModeEnabled ? StandardToMQRMetrics.isStandardMetric(metricKeysByUuids.get(c.getMetricUuid()))
+ : StandardToMQRMetrics.isMQRMetric(metricKeysByUuids.get(c.getMetricUuid())));
+
+ if (hasConditionsFromOtherMode) {
+ notificationManager.scheduleForSending(new QualityGateMetricsUpdateNotification(isMQRModeEnabled));
+ }
+ }
+ }
+
+ private void sendNewModesNotification(boolean isMQRModeEnabled) {
+ notificationManager.scheduleForSending(new MQRAndStandardModesExistNotification(isMQRModeEnabled));
+ }
+
+ @Override
+ public void stop() {
+ // Nothing to do
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotification.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotification.java
new file mode 100644
index 00000000000..a406d4a3eec
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotification.java
@@ -0,0 +1,29 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+public class QualityGateMetricsUpdateNotification extends AbstractModeNotification {
+ public static final String TYPE = "update-quality-gate-metrics";
+
+ public QualityGateMetricsUpdateNotification(boolean isMQRModeEnabled) {
+ super(TYPE, isMQRModeEnabled);
+
+ }
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandler.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandler.java
new file mode 100644
index 00000000000..81033f72762
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandler.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.Collection;
+import java.util.Optional;
+import java.util.Set;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.server.notification.EmailNotificationHandler;
+import org.sonar.server.notification.NotificationDispatcherMetadata;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+import static java.util.stream.Collectors.toSet;
+
+public class QualityGateMetricsUpdateNotificationHandler extends EmailNotificationHandler<QualityGateMetricsUpdateNotification> {
+ private final DbClient dbClient;
+
+ protected QualityGateMetricsUpdateNotificationHandler(DbClient dbClient, EmailNotificationChannel emailChannel) {
+ super(emailChannel);
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ protected Set<EmailNotificationChannel.EmailDeliveryRequest> toEmailDeliveryRequests(Collection<QualityGateMetricsUpdateNotification> notifications) {
+ try (DbSession session = dbClient.openSession(false)) {
+ return dbClient.authorizationDao()
+ .selectQualityGateAdministratorLogins(session)
+ .stream()
+ .flatMap(t -> notifications.stream().map(notification -> new EmailNotificationChannel.EmailDeliveryRequest(t.getEmail(), notification)))
+ .collect(toSet());
+ }
+ }
+
+ @Override
+ public Optional<NotificationDispatcherMetadata> getMetadata() {
+ return Optional.empty();
+ }
+
+ @Override
+ public Class<QualityGateMetricsUpdateNotification> getNotificationClass() {
+ return QualityGateMetricsUpdateNotification.class;
+ }
+
+}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplate.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplate.java
new file mode 100644
index 00000000000..6a06bdd2f3b
--- /dev/null
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplate.java
@@ -0,0 +1,58 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.sonar.api.notifications.Notification;
+
+public class QualityGateMetricsUpdateTemplate implements EmailTemplate {
+
+ private static final String STANDARD_EXPERIENCE = "Standard Experience";
+ private static final String MQR_MODE = "Multi-Quality Rule (MQR) Mode";
+
+ @Nullable
+ @Override
+ public EmailMessage format(Notification notification) {
+ if (!QualityGateMetricsUpdateNotification.TYPE.equals(notification.getType())) {
+ return null;
+ }
+
+ String message = retrieveMessage((QualityGateMetricsUpdateNotification) notification);
+
+ // And finally return the email that will be sent
+ return new EmailMessage()
+ .setMessageId(MQRAndStandardModesExistNotification.TYPE)
+ .setSubject("Update your SonarQube Server's Quality Gate metrics")
+ .setPlainTextMessage(message);
+ }
+
+ @NotNull
+ private static String retrieveMessage(QualityGateMetricsUpdateNotification notification) {
+ StringBuilder message = new StringBuilder();
+ message.append("We are sending this message because this version of SonarQube Server is in ");
+ message.append(notification.isMQRModeEnabled() ? MQR_MODE : STANDARD_EXPERIENCE);
+ message.append(" and some of your quality gates conditions are using metrics from ");
+ message.append(notification.isMQRModeEnabled() ? STANDARD_EXPERIENCE : MQR_MODE);
+ message.append(".\n\nWe recommend you update them to ensure accurate categorization and ranking of your issues.\n\n");
+ message.append("Go to the Quality Gates page, and we will guide you through the process.");
+ return message.toString();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandlerTest.java
new file mode 100644
index 00000000000..99922fc0897
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationHandlerTest.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.List;
+import java.util.Set;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.EmailSubscriberDto;
+import org.sonar.db.permission.AuthorizationDao;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MQRAndStandardModesExistNotificationHandlerTest {
+ private final DbClient dbClient = mock(DbClient.class);
+ private final DbSession dbSession = mock(DbSession.class);
+ private final AuthorizationDao authorizationDao = mock(AuthorizationDao.class);
+ private final EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
+
+ private final MQRAndStandardModesExistNotificationHandler underTest = new MQRAndStandardModesExistNotificationHandler(dbClient, emailNotificationChannel);
+
+ @BeforeEach
+ public void wire_mocks() {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.authorizationDao()).thenReturn(authorizationDao);
+ }
+
+ @Test
+ void toEmailDeliveryRequests_whenHasAdmins_shouldSendExpectedNotification() {
+ when(authorizationDao.selectGlobalAdministerEmailSubscribers(dbSession))
+ .thenReturn(Set.of(new EmailSubscriberDto().setEmail("email@email.com"), new EmailSubscriberDto().setEmail("email2@email.com")));
+
+ Assertions.assertThat(underTest.toEmailDeliveryRequests(List.of(new MQRAndStandardModesExistNotification(true))))
+ .extracting(EmailNotificationChannel.EmailDeliveryRequest::recipientEmail, EmailNotificationChannel.EmailDeliveryRequest::notification)
+ .containsExactly(tuple("email@email.com", new MQRAndStandardModesExistNotification(true)),
+ tuple("email2@email.com", new MQRAndStandardModesExistNotification(true)));
+ }
+
+ @Test
+ void toEmailDeliveryRequests_whenHasNoAdmins_shouldNotSendNotification() {
+ when(authorizationDao.selectGlobalAdministerEmailSubscribers(dbSession))
+ .thenReturn(Set.of());
+
+ Assertions.assertThat(underTest.toEmailDeliveryRequests(List.of(new MQRAndStandardModesExistNotification(true))))
+ .isEmpty();
+ }
+
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationTest.java
new file mode 100644
index 00000000000..0bf9f5928c1
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistNotificationTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class MQRAndStandardModesExistNotificationTest {
+
+ @Test
+ void isMQRModeEnabled_shouldReturnExpectedValue() {
+ MQRAndStandardModesExistNotification underTest = new MQRAndStandardModesExistNotification(false);
+ Assertions.assertThat(underTest.isMQRModeEnabled()).isFalse();
+
+ underTest = new MQRAndStandardModesExistNotification(true);
+ Assertions.assertThat(underTest.isMQRModeEnabled()).isTrue();
+ }
+
+ @Test
+ void equals_shouldReturnAsExpected() {
+ MQRAndStandardModesExistNotification underTest = new MQRAndStandardModesExistNotification(false);
+ Assertions.assertThat(underTest.equals(new MQRAndStandardModesExistNotification(false))).isTrue();
+ Assertions.assertThat(underTest.equals(new MQRAndStandardModesExistNotification(true))).isFalse();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplateTest.java
new file mode 100644
index 00000000000..722001ebbd3
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/MQRAndStandardModesExistTemplateTest.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.sonar.api.notifications.Notification;
+
+class MQRAndStandardModesExistTemplateTest {
+
+ private final MQRAndStandardModesExistTemplate underTest = new MQRAndStandardModesExistTemplate();
+
+ @Test
+ void format_whenStandardExperience_shouldReturnExpectEmailMessage() {
+ Assertions.assertThat(underTest.format(new MQRAndStandardModesExistNotification(false)))
+ .extracting(EmailMessage::getSubject, EmailMessage::getMessage)
+ .containsExactly("Your SonarQube Server instance is in Standard Experience",
+ """
+ In this version of SonarQube Server, there are two options to reflect the health of all the projects: Multi-Quality Rule (MQR) Mode and Standard Experience.
+ The SonarQube Server documentation explains more.
+
+ Your instance is currently using the Standard Experience.
+
+ To change it, go to Administration > Configuration > General Settings > Mode.
+ """);
+ }
+
+ @Test
+ void format_whenMQRMode_shouldReturnExpectEmailMessage() {
+ Assertions.assertThat(underTest.format(new MQRAndStandardModesExistNotification(true)))
+ .extracting(EmailMessage::getSubject, EmailMessage::getMessage)
+ .containsExactly("Your SonarQube Server instance is in Multi-Quality Rule (MQR) Mode",
+ """
+ In this version of SonarQube Server, there are two options to reflect the health of all the projects: Multi-Quality Rule (MQR) Mode and Standard Experience.
+ The SonarQube Server documentation explains more.
+
+ Your instance is currently using the Multi-Quality Rule (MQR) Mode.
+
+ To change it, go to Administration > Configuration > General Settings > Mode.
+ """);
+ }
+
+ @Test
+ void format_whenInvalidNotification_shouldReturnNull() {
+ Assertions.assertThat(underTest.format(new TestNotification())).isNull();
+ }
+
+ private static class TestNotification extends Notification {
+ public TestNotification() {
+ super("test");
+ }
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsModuleTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsModuleTest.java
new file mode 100644
index 00000000000..4ac667ea5cc
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsModuleTest.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.junit.jupiter.api.Test;
+import org.sonar.core.platform.ListContainer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class NewModesNotificationsModuleTest {
+
+ @Test
+ void configure_shouldReturnExpectedNumberOfComponents() {
+ ListContainer container = new ListContainer();
+ new NewModesNotificationsModule().configure(container);
+ assertThat(container.getAddedObjects()).hasSize(7);
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsSenderTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsSenderTest.java
new file mode 100644
index 00000000000..6583ff25599
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/NewModesNotificationsSenderTest.java
@@ -0,0 +1,160 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.notifications.Notification;
+import org.sonar.core.metric.SoftwareQualitiesMetrics;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.metric.MetricDao;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.qualitygate.QualityGateConditionDao;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+import org.sonar.server.notification.NotificationManager;
+import org.sonar.server.platform.db.migration.history.MigrationHistory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.config.MQRModeConstants.MULTI_QUALITY_MODE_ENABLED;
+
+class NewModesNotificationsSenderTest {
+ private static final String METRIC_UUID = "metricUuid";
+ private static final String METRIC_UUID_2 = "metricUuid2";
+ private final NotificationManager notificationManager = mock(NotificationManager.class);
+ private final Configuration configuration = mock(Configuration.class);
+ private final MigrationHistory migrationHistory = mock(MigrationHistory.class);
+ private final DbClient dbClient = mock(DbClient.class);
+ private final MetricDao metricDao = mock(MetricDao.class);
+ private final QualityGateConditionDao qualityGateConditionDao = mock(QualityGateConditionDao.class);
+ private final NewModesNotificationsSender underTest = new NewModesNotificationsSender(notificationManager, configuration, migrationHistory, dbClient);
+ private final DbSession dbSession = mock(DbSession.class);
+
+ @BeforeEach
+ void setUp() {
+ when(dbClient.metricDao()).thenReturn(metricDao);
+ when(dbClient.gateConditionDao()).thenReturn(qualityGateConditionDao);
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ }
+
+ @Test
+ void start_whenOldInstanceAndStandardMode_shouldSendNewModesNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(false));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(9999L); // 9.9
+ underTest.start();
+
+ ArgumentCaptor<MQRAndStandardModesExistNotification> captor = ArgumentCaptor.forClass(MQRAndStandardModesExistNotification.class);
+ verify(notificationManager, times(1)).scheduleForSending(captor.capture());
+
+ assertThat(captor.getValue().isMQRModeEnabled()).isFalse();
+ }
+
+ @Test
+ void start_whenOldInstanceAndMQRMode_shouldSendNewModesNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(true));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(102000L); // 10.2
+
+ underTest.start();
+
+ ArgumentCaptor<MQRAndStandardModesExistNotification> captor = ArgumentCaptor.forClass(MQRAndStandardModesExistNotification.class);
+ verify(notificationManager, times(1)).scheduleForSending(captor.capture());
+
+ assertThat(captor.getValue().isMQRModeEnabled()).isTrue();
+ }
+
+ @Test
+ void start_whenNewInstance_shouldNotSendNewModesNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(true));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(-1L); // New instance
+
+ underTest.start();
+ verifyNoInteractions(notificationManager);
+ }
+
+ @Test
+ void start_whenOldInstanceInStandardModeWithMQRConditions_shouldSendQualityGateUpdateNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(false));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(9999L); // 9.9
+ when(metricDao.selectAll(dbSession)).thenReturn(List.of(new MetricDto().setKey(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_ISSUES_KEY).setUuid(METRIC_UUID)));
+ when(qualityGateConditionDao.selectAll(dbSession)).thenReturn(List.of(new QualityGateConditionDto().setMetricUuid(METRIC_UUID)));
+
+ underTest.start();
+
+ ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+ verify(notificationManager, times(2)).scheduleForSending(captor.capture());
+
+ assertThat(captor.getAllValues())
+ .filteredOn(notification -> notification instanceof QualityGateMetricsUpdateNotification)
+ .map(notification -> (QualityGateMetricsUpdateNotification) notification)
+ .hasSize(1)
+ .extracting(QualityGateMetricsUpdateNotification::isMQRModeEnabled).isEqualTo(List.of(false));
+
+ }
+
+ @Test
+ void start_whenOldInstanceInMQRModeWithStandardConditions_shouldSendQualityGateUpdateNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(true));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(9999L); // 9.9
+ when(metricDao.selectAll(dbSession)).thenReturn(List.of(new MetricDto().setKey(CoreMetrics.CODE_SMELLS_KEY).setUuid(METRIC_UUID)));
+ when(qualityGateConditionDao.selectAll(dbSession)).thenReturn(List.of(new QualityGateConditionDto().setMetricUuid(METRIC_UUID)));
+
+ underTest.start();
+
+ ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+ verify(notificationManager, times(2)).scheduleForSending(captor.capture());
+
+ assertThat(captor.getAllValues())
+ .filteredOn(notification -> notification instanceof QualityGateMetricsUpdateNotification)
+ .map(notification -> (QualityGateMetricsUpdateNotification) notification)
+ .hasSize(1)
+ .extracting(QualityGateMetricsUpdateNotification::isMQRModeEnabled).isEqualTo(List.of(true));
+ }
+
+ @Test
+ void start_whenOldInstanceInMQRModeWithOtherConditions_shouldNotSendNotification() {
+ when(configuration.getBoolean(MULTI_QUALITY_MODE_ENABLED)).thenReturn(Optional.of(true));
+ when(migrationHistory.getInitialDbVersion()).thenReturn(9999L); // 9.9
+ when(metricDao.selectAll(dbSession)).thenReturn(List.of(
+ new MetricDto().setKey(CoreMetrics.COVERAGE_KEY).setUuid(METRIC_UUID),
+ new MetricDto().setKey(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).setUuid(METRIC_UUID_2)));
+ when(qualityGateConditionDao.selectAll(dbSession)).thenReturn(List.of(new QualityGateConditionDto().setMetricUuid(METRIC_UUID),
+ new QualityGateConditionDto().setMetricUuid(METRIC_UUID_2)));
+
+ underTest.start();
+
+ ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
+ verify(notificationManager, times(1)).scheduleForSending(captor.capture());
+
+ assertThat(captor.getAllValues())
+ .filteredOn(notification -> notification instanceof QualityGateMetricsUpdateNotification)
+ .map(notification -> (QualityGateMetricsUpdateNotification) notification)
+ .isEmpty();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandlerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandlerTest.java
new file mode 100644
index 00000000000..6dda1019c1a
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationHandlerTest.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import java.util.List;
+import java.util.Set;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.EmailSubscriberDto;
+import org.sonar.db.permission.AuthorizationDao;
+import org.sonar.server.notification.email.EmailNotificationChannel;
+
+import static org.assertj.core.groups.Tuple.tuple;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class QualityGateMetricsUpdateNotificationHandlerTest {
+ private final DbClient dbClient = mock(DbClient.class);
+ private final DbSession dbSession = mock(DbSession.class);
+ private final AuthorizationDao authorizationDao = mock(AuthorizationDao.class);
+ private final EmailNotificationChannel emailNotificationChannel = mock(EmailNotificationChannel.class);
+
+ private final QualityGateMetricsUpdateNotificationHandler underTest = new QualityGateMetricsUpdateNotificationHandler(dbClient,
+ emailNotificationChannel);
+
+ @BeforeEach
+ public void wire_mocks() {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.authorizationDao()).thenReturn(authorizationDao);
+ }
+
+ @Test
+ void toEmailDeliveryRequests_whenHasAdmins_shouldSendExpectedNotification() {
+ when(authorizationDao.selectQualityGateAdministratorLogins(dbSession))
+ .thenReturn(Set.of(new EmailSubscriberDto().setEmail("email@email.com"), new EmailSubscriberDto().setEmail("email2@email.com")));
+
+ Assertions.assertThat(underTest.toEmailDeliveryRequests(List.of(new QualityGateMetricsUpdateNotification(true))))
+ .extracting(EmailNotificationChannel.EmailDeliveryRequest::recipientEmail, EmailNotificationChannel.EmailDeliveryRequest::notification)
+ .containsExactly(tuple("email@email.com", new QualityGateMetricsUpdateNotification(true)),
+ tuple("email2@email.com", new QualityGateMetricsUpdateNotification(true)));
+ }
+
+ @Test
+ void toEmailDeliveryRequests_whenHasNoAdmins_shouldNotSendNotification() {
+ when(authorizationDao.selectQualityGateAdministratorLogins(dbSession))
+ .thenReturn(Set.of());
+
+ Assertions.assertThat(underTest.toEmailDeliveryRequests(List.of(new QualityGateMetricsUpdateNotification(true))))
+ .isEmpty();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationTest.java
new file mode 100644
index 00000000000..1f9367b6677
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateNotificationTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class QualityGateMetricsUpdateNotificationTest {
+
+ @Test
+ void isMQRModeEnabled_shouldReturnExpectedValue() {
+ QualityGateMetricsUpdateNotification underTest = new QualityGateMetricsUpdateNotification(false);
+ Assertions.assertThat(underTest.isMQRModeEnabled()).isFalse();
+
+ underTest = new QualityGateMetricsUpdateNotification(true);
+ Assertions.assertThat(underTest.isMQRModeEnabled()).isTrue();
+ }
+
+ @Test
+ void equals_shouldReturnAsExpected() {
+ QualityGateMetricsUpdateNotification underTest = new QualityGateMetricsUpdateNotification(false);
+ Assertions.assertThat(underTest.equals(new QualityGateMetricsUpdateNotification(false))).isTrue();
+ Assertions.assertThat(underTest.equals(new QualityGateMetricsUpdateNotification(true))).isFalse();
+ }
+}
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplateTest.java
new file mode 100644
index 00000000000..c391b19ec82
--- /dev/null
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/notification/QualityGateMetricsUpdateTemplateTest.java
@@ -0,0 +1,66 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2024 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.issue.notification;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.sonar.api.notifications.Notification;
+
+class QualityGateMetricsUpdateTemplateTest {
+
+ QualityGateMetricsUpdateTemplate underTest = new QualityGateMetricsUpdateTemplate();
+
+ @Test
+ void format_whenStandardExperience_shouldReturnExpectEmailMessage() {
+ Assertions.assertThat(underTest.format(new QualityGateMetricsUpdateNotification(false)))
+ .extracting(EmailMessage::getSubject, EmailMessage::getMessage)
+ .containsExactly("Update your SonarQube Server's Quality Gate metrics",
+ """
+ We are sending this message because this version of SonarQube Server is in Standard Experience and some of your quality gates conditions are using metrics from Multi-Quality Rule (MQR) Mode.
+
+ We recommend you update them to ensure accurate categorization and ranking of your issues.
+
+ Go to the Quality Gates page, and we will guide you through the process.""");
+ }
+
+ @Test
+ void format_whenMQRMode_shouldReturnExpectEmailMessage() {
+ Assertions.assertThat(underTest.format(new QualityGateMetricsUpdateNotification(true)))
+ .extracting(EmailMessage::getSubject, EmailMessage::getMessage)
+ .containsExactly("Update your SonarQube Server's Quality Gate metrics",
+ """
+ We are sending this message because this version of SonarQube Server is in Multi-Quality Rule (MQR) Mode and some of your quality gates conditions are using metrics from Standard Experience.
+
+ We recommend you update them to ensure accurate categorization and ranking of your issues.
+
+ Go to the Quality Gates page, and we will guide you through the process.""");
+ }
+
+ @Test
+ void format_whenInvalidNotification_shouldReturnNull() {
+ Assertions.assertThat(underTest.format(new TestNotification())).isNull();
+ }
+
+ private static class TestNotification extends Notification {
+ public TestNotification() {
+ super("test");
+ }
+ }
+}
diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
index 9da8b6da3a4..a09c93f5d68 100644
--- a/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
+++ b/server/sonar-webserver/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
@@ -41,7 +41,6 @@ import org.sonar.alm.client.gitlab.GitlabApplicationHttpClient;
import org.sonar.alm.client.gitlab.GitlabGlobalSettingsValidator;
import org.sonar.alm.client.gitlab.GitlabHeaders;
import org.sonar.alm.client.gitlab.GitlabPaginatedHttpClient;
-import org.sonar.server.component.ComponentTypes;
import org.sonar.api.server.rule.RulesDefinitionXmlLoader;
import org.sonar.auth.bitbucket.BitbucketModule;
import org.sonar.auth.github.GitHubModule;
@@ -54,7 +53,6 @@ import org.sonar.ce.task.projectanalysis.taskprocessor.AuditPurgeTaskProcessor;
import org.sonar.ce.task.projectanalysis.taskprocessor.IssueSyncTaskProcessor;
import org.sonar.ce.task.projectanalysis.taskprocessor.ReportTaskProcessor;
import org.sonar.ce.task.projectexport.taskprocessor.ProjectExportTaskProcessor;
-import org.sonar.server.component.DefaultComponentTypes;
import org.sonar.core.documentation.DefaultDocumentationLinkGenerator;
import org.sonar.core.extension.CoreExtensionsInstaller;
import org.sonar.core.language.LanguagesProvider;
@@ -108,6 +106,8 @@ import org.sonar.server.common.text.MacroInterpreter;
import org.sonar.server.component.ComponentCleanerService;
import org.sonar.server.component.ComponentFinder;
import org.sonar.server.component.ComponentService;
+import org.sonar.server.component.ComponentTypes;
+import org.sonar.server.component.DefaultComponentTypes;
import org.sonar.server.component.index.ComponentIndex;
import org.sonar.server.component.index.ComponentIndexDefinition;
import org.sonar.server.component.index.EntityDefinitionIndexer;
@@ -151,6 +151,7 @@ import org.sonar.server.issue.notification.MyNewIssuesEmailTemplate;
import org.sonar.server.issue.notification.MyNewIssuesNotificationHandler;
import org.sonar.server.issue.notification.NewIssuesEmailTemplate;
import org.sonar.server.issue.notification.NewIssuesNotificationHandler;
+import org.sonar.server.issue.notification.NewModesNotificationsModule;
import org.sonar.server.issue.ws.IssueWsModule;
import org.sonar.server.language.LanguageValidation;
import org.sonar.server.language.ws.LanguageWs;
@@ -292,8 +293,8 @@ import org.sonar.server.webhook.WebhookQGChangeEventListener;
import org.sonar.server.webhook.ws.WebhooksWsModule;
import org.sonar.server.ws.WebServiceEngine;
import org.sonar.server.ws.ws.WebServicesWsModule;
-import org.sonar.telemetry.core.TelemetryClient;
import org.sonar.telemetry.TelemetryDaemon;
+import org.sonar.telemetry.core.TelemetryClient;
import org.sonar.telemetry.legacy.CloudUsageDataProvider;
import org.sonar.telemetry.legacy.ProjectLocDistributionDataProvider;
import org.sonar.telemetry.legacy.QualityProfileDataProvider;
@@ -537,6 +538,7 @@ public class PlatformLevel4 extends PlatformLevel {
BuiltInQPChangeNotificationTemplate.class,
BuiltInQPChangeNotificationHandler.class,
+ new NewModesNotificationsModule(),
new NotificationModule(),
new NotificationWsModule(),
new EmailsWsModule(),