Merge pull request #2420 from bluewave-labs/feat/test-notification

feat: test notification
This commit is contained in:
Alexander Holliday
2025-06-10 13:49:16 +08:00
committed by GitHub
9 changed files with 197 additions and 93 deletions

View File

@@ -152,10 +152,36 @@ const useEditNotification = () => {
return [editNotification, isLoading, error];
};
const useTestNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const testNotification = async (notification) => {
try {
setIsLoading(true);
await networkService.testNotification({ notification });
createToast({
body: t("notifications.test.success"),
});
} catch (error) {
setError(error);
createToast({
body: t("notifications.test.failed"),
});
} finally {
setIsLoading(false);
}
};
return [testNotification, isLoading, error];
};
export {
useCreateNotification,
useGetNotificationsByTeamId,
useDeleteNotification,
useGetNotificationById,
useEditNotification,
useTestNotification,
};

View File

@@ -16,6 +16,7 @@ import {
useCreateNotification,
useGetNotificationById,
useEditNotification,
useTestNotification,
} from "../../../Hooks/useNotifications";
import {
notificationEmailValidation,
@@ -35,6 +36,8 @@ const CreateNotifications = () => {
const [createNotification, isCreating, createNotificationError] =
useCreateNotification();
const [editNotification, isEditing, editNotificationError] = useEditNotification();
const [testNotification, isTesting, testNotificationError] = useTestNotification();
const BREADCRUMBS = [
{ name: "notifications", path: "/notifications" },
{ name: "create", path: "/notifications/create" },
@@ -178,6 +181,56 @@ const CreateNotifications = () => {
setNotification(newNotification);
};
const onTestNotification = () => {
const form = {
...notification,
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
if (notification.type === 2) {
form.type = "webhook";
}
let error = null;
if (form.type === "email") {
error = notificationEmailValidation.validate(
{ notificationName: form.notificationName, address: form.address },
{ abortEarly: false }
).error;
} else if (form.type === "webhook") {
form.config = {
platform: form.config.platform,
webhookUrl: form.config.webhookUrl,
};
error = notificationWebhookValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
} else if (form.type === "pager_duty") {
form.config = {
platform: form.config.platform,
routingKey: form.config.routingKey,
};
error = notificationPagerDutyValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
}
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
createToast({ body: "Please check the form for errors." });
setErrors(newErrors);
return;
}
testNotification(form);
};
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
@@ -357,7 +410,16 @@ const CreateNotifications = () => {
<Stack
direction="row"
justifyContent="flex-end"
spacing={theme.spacing(2)}
>
<Button
loading={isCreating || isEditing || notificationIsLoading}
variant="contained"
color="secondary"
onClick={onTestNotification}
>
Test notification
</Button>
<Button
loading={isCreating || isEditing || notificationIsLoading}
type="submit"

View File

@@ -56,7 +56,16 @@ const Notifications = () => {
id: "target",
content: "Target",
render: (row) => {
return row.address || row.config?.webhookUrl || row.config?.routingKey;
const type = row.type;
if (type === "email") {
return row.address;
}
if (type === "webhook") {
return row.config?.webhookUrl;
}
if (type === "pager_duty") {
return row.config?.routingKey;
}
},
},
{

View File

@@ -703,18 +703,11 @@ class NetworkService {
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async testNotification(config) {
return this.axiosInstance.post(
"/notifications/test-webhook",
{
platform: config.platform,
...config.payload,
return this.axiosInstance.post("/notifications/test", config.notification, {
headers: {
"Content-Type": "application/json",
},
{
headers: {
"Content-Type": "application/json",
},
}
);
});
}
/**

View File

@@ -363,6 +363,10 @@
"edit": {
"success": "Notification updated successfully",
"failed": "Failed to update notification"
},
"test": {
"success": "Test notification sent successfully",
"failed": "Failed to send test notification"
}
},
"testLocale": "testLocale",

View File

@@ -24,7 +24,6 @@ class NotificationController {
this.statusService = statusService;
this.db = db;
this.triggerNotification = this.triggerNotification.bind(this);
this.testWebhook = this.testWebhook.bind(this);
}
async triggerNotification(req, res, next) {
@@ -85,94 +84,38 @@ class NotificationController {
};
}
handleTelegramTest(botToken, chatId) {
if (!botToken || !chatId) {
return {
isValid: false,
error: {
msg: this.stringService.telegramRequiresBotTokenAndChatId,
status: 400,
},
};
}
return {
isValid: true,
notification: {
type: NOTIFICATION_TYPES.WEBHOOK,
platform: PLATFORMS.TELEGRAM,
config: { botToken, chatId },
},
};
}
handleWebhookTest(webhookUrl, platform) {
if (webhookUrl === null) {
return {
isValid: false,
error: {
msg: this.stringService.webhookUrlRequired,
status: 400,
},
};
}
return {
isValid: true,
notification: {
type: NOTIFICATION_TYPES.WEBHOOK,
platform: platform,
config: { webhookUrl },
},
};
}
async testWebhook(req, res, next) {
testNotification = async (req, res, next) => {
try {
const { webhookUrl, platform, botToken, chatId } = req.body;
const notification = req.body;
let success;
if (platform === null) {
if (notification?.type === "email") {
success = await this.notificationService.sendTestEmail(notification);
}
if (notification?.type === "webhook") {
success =
await this.notificationService.sendTestWebhookNotification(notification);
}
if (notification?.type === "pager_duty") {
success =
await this.notificationService.sendTestPagerDutyNotification(notification);
}
if (!success) {
return res.error({
msg: this.stringService.platformRequired,
msg: "Sending notification failed",
status: 400,
});
}
// Platform-specific handling
const platformHandlers = {
[PLATFORMS.TELEGRAM]: () => this.handleTelegramTest(botToken, chatId),
// Default handler for webhook-based platforms (Slack, Discord, etc.)
default: () => this.handleWebhookTest(webhookUrl, platform),
};
const handler = platformHandlers[platform] || platformHandlers.default;
const handlerResult = handler();
if (!handlerResult.isValid) {
return res.error(handlerResult.error);
}
const networkResponse = this.createTestNetworkResponse();
const result = await this.notificationService.sendWebhookNotification(
networkResponse,
handlerResult.notification
);
if (result && result !== false) {
return res.success({
msg: this.stringService.webhookSendSuccess,
});
} else {
return res.error({
msg: this.stringService.testNotificationFailed,
status: 400,
});
}
return res.success({
msg: "Notification sent successfully",
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "testWebhook"));
}
}
};
createNotification = async (req, res, next) => {
try {

View File

@@ -13,7 +13,7 @@ class NotificationRoutes {
this.router.post("/trigger", this.notificationController.triggerNotification);
this.router.post("/test-webhook", this.notificationController.testWebhook);
this.router.post("/test", this.notificationController.testNotification);
this.router.post("/", this.notificationController.createNotification);

View File

@@ -463,6 +463,29 @@ class NetworkService {
}
}
async requestPagerDuty({ message, routingKey, monitorUrl }) {
try {
const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, {
routing_key: routingKey,
event_action: "trigger",
payload: {
summary: message,
severity: "critical",
source: monitorUrl,
timestamp: new Date().toISOString(),
},
});
if (response?.data?.status !== "success") return false;
return true;
} catch (error) {
error.details = error.response?.data;
error.service = this.SERVICE_NAME;
error.method = "requestPagerDuty";
throw error;
}
}
/**
* Gets the status of a job based on its type and returns the appropriate response.
*

View File

@@ -95,6 +95,17 @@ class NotificationService {
return MESSAGE_FORMATTERS[platform](messageText, chatId);
}
sendTestWebhookNotification = async (notification) => {
const config = notification.config;
const response = await this.networkService.requestWebhook(
config.platform,
config.webhookUrl,
"This is a test notification"
);
return response.status;
};
/**
* Sends a webhook notification to a specified platform.
*
@@ -185,6 +196,28 @@ class NotificationService {
return true;
}
sendTestEmail = async (notification) => {
const to = notification?.address;
if (!to || typeof to !== "string") {
throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
const messageId = await this.emailService.buildAndSendEmail(
"testEmailTemplate",
context,
to,
subject
);
if (messageId) {
return true;
}
return false;
};
/**
* Sends an email notification about monitor status change
*
@@ -194,6 +227,7 @@ class NotificationService {
* @param {string} address - Email address to send the notification to
* @returns {Promise<boolean>} - Indicates email was sent successfully
*/
async sendEmail(networkResponse, address) {
const { monitor, status, prevStatus } = networkResponse;
const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
@@ -203,6 +237,16 @@ class NotificationService {
return true;
}
async sendTestPagerDutyNotification(notification) {
const { routingKey } = notification.config;
const response = await this.networkService.requestPagerDuty({
message: "This is a test notification",
monitorUrl: "Test notification",
routingKey,
});
return response;
}
async sendPagerDutyNotification(networkResponse, notification) {
const { monitor, status, code } = networkResponse;
const { routingKey, platform } = notification.config;
@@ -222,7 +266,7 @@ class NotificationService {
routingKey,
monitorUrl: monitor.url,
});
return response.status;
return response;
} catch (error) {
this.logger.error({
message: "Failed to send PagerDuty notification",