Merge d950ec699f into 7136dd7832
commit
430af2f362
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Add status_page_subscriber management system for status page notifications
|
||||
* - Email subscription to status pages
|
||||
* - Email verification workflow
|
||||
* - Subscription preferences
|
||||
* - Notification queue
|
||||
*/
|
||||
|
||||
exports.up = function (knex) {
|
||||
return (
|
||||
knex.schema
|
||||
// Create status_page_subscriber table
|
||||
.createTable("status_page_subscriber", (table) => {
|
||||
table.increments("id").primary();
|
||||
table.string("email", 255).notNullable().unique();
|
||||
table.string("unsubscribe_token", 255).unique();
|
||||
table.timestamps(false, true);
|
||||
|
||||
table.index("email", "subscriber_email");
|
||||
table.index("unsubscribe_token", "subscriber_unsubscribe_token");
|
||||
})
|
||||
// Create status_page_subscription table (links subscribers to status pages)
|
||||
.createTable("status_page_subscription", (table) => {
|
||||
table.increments("id").primary();
|
||||
table
|
||||
.integer("subscriber_id")
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references("id")
|
||||
.inTable("status_page_subscriber")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE");
|
||||
table
|
||||
.integer("status_page_id")
|
||||
.unsigned()
|
||||
.notNullable()
|
||||
.references("id")
|
||||
.inTable("status_page")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE");
|
||||
table.boolean("notify_incidents").defaultTo(true);
|
||||
table.boolean("notify_maintenance").defaultTo(true);
|
||||
table.boolean("notify_status_changes").defaultTo(false);
|
||||
table.boolean("verified").defaultTo(false);
|
||||
table.string("verification_token", 255);
|
||||
table.timestamps(false, true);
|
||||
|
||||
table.index("status_page_id", "status_page_subscription_status_page_id");
|
||||
table.index("verification_token", "status_page_subscription_verification_token");
|
||||
|
||||
table.unique(["subscriber_id", "status_page_id"]);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.dropTableIfExists("status_page_subscription").dropTableIfExists("status_page_subscriber");
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
exports.up = async function (knex) {
|
||||
await knex.schema.alterTable("status_page", function (table) {
|
||||
table.string("notification_email", 255);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.alterTable("status_page", function (table) {
|
||||
table.dropColumn("notification_email");
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const { nanoid } = require("nanoid");
|
||||
|
||||
/**
|
||||
* StatusPageSubscriber model
|
||||
* Represents email subscribers to status pages
|
||||
*/
|
||||
class StatusPageSubscriber extends BeanModel {
|
||||
/**
|
||||
* Generate unsubscribe token
|
||||
* @returns {string} Unsubscribe token
|
||||
*/
|
||||
static generateUnsubscribeToken() {
|
||||
return nanoid(32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for admin
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
email: this.email,
|
||||
createdAt: this.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON for public
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toPublicJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find status_page_subscriber by email
|
||||
* @param {string} email Email address
|
||||
* @returns {Promise<StatusPageSubscriber|null>} StatusPageSubscriber or null
|
||||
*/
|
||||
static async findByEmail(email) {
|
||||
return await R.findOne("status_page_subscriber", " email = ? ", [email]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find status_page_subscriber by unsubscribe token
|
||||
* @param {string} token Unsubscribe token
|
||||
* @returns {Promise<StatusPageSubscriber|null>} StatusPageSubscriber or null
|
||||
*/
|
||||
static async findByUnsubscribeToken(token) {
|
||||
return await R.findOne("status_page_subscriber", " unsubscribe_token = ? ", [token]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StatusPageSubscriber;
|
||||
@ -0,0 +1,86 @@
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { R } = require("redbean-node");
|
||||
const { nanoid } = require("nanoid");
|
||||
|
||||
/**
|
||||
* StatusPageSubscription model
|
||||
* Links subscribers to status pages
|
||||
*/
|
||||
class StatusPageSubscription extends BeanModel {
|
||||
/**
|
||||
* Generate verification token
|
||||
* @returns {string} Verification token
|
||||
*/
|
||||
static generateVerificationToken() {
|
||||
return nanoid(32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the status_page_subscription
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async verify() {
|
||||
this.verified = true;
|
||||
this.verification_token = null;
|
||||
await R.store(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find status_page_subscription by verification token
|
||||
* @param {string} token Verification token
|
||||
* @returns {Promise<StatusPageSubscription|null>} StatusPageSubscription or null
|
||||
*/
|
||||
static async findByVerificationToken(token) {
|
||||
return await R.findOne("status_page_subscription", " verification_token = ? ", [token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an object that ready to parse to JSON
|
||||
* @returns {object} Object ready to parse
|
||||
*/
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
subscriberId: this.subscriber_id,
|
||||
statusPageId: this.status_page_id,
|
||||
notifyIncidents: !!this.notify_incidents,
|
||||
notifyMaintenance: !!this.notify_maintenance,
|
||||
notifyStatusChanges: !!this.notify_status_changes,
|
||||
verified: !!this.verified,
|
||||
createdAt: this.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subscriptions for a subscriber
|
||||
* @param {number} subscriberId Subscriber ID
|
||||
* @returns {Promise<StatusPageSubscription[]>} Array of subscriptions
|
||||
*/
|
||||
static async getBySubscriber(subscriberId) {
|
||||
return await R.find("status_page_subscription", " subscriber_id = ? ", [subscriberId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subscriptions for a status page
|
||||
* @param {number} statusPageId Status Page ID
|
||||
* @returns {Promise<StatusPageSubscription[]>} Array of subscriptions
|
||||
*/
|
||||
static async getByStatusPage(statusPageId) {
|
||||
return await R.find("status_page_subscription", " status_page_id = ? ", [statusPageId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if status_page_subscription exists
|
||||
* @param {number} subscriberId Subscriber ID
|
||||
* @param {number} statusPageId Status Page ID
|
||||
* @returns {Promise<StatusPageSubscription|null>} StatusPageSubscription or null
|
||||
*/
|
||||
static async exists(subscriberId, statusPageId) {
|
||||
return await R.findOne("status_page_subscription", " subscriber_id = ? AND status_page_id = ? ", [
|
||||
subscriberId,
|
||||
statusPageId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StatusPageSubscription;
|
||||
@ -0,0 +1,133 @@
|
||||
const { R } = require("redbean-node");
|
||||
const { log } = require("../src/util");
|
||||
const { Notification } = require("./notification");
|
||||
|
||||
/**
|
||||
* Send a status page notification email to a configured address.
|
||||
* Uses the admin's default or first SMTP notification provider.
|
||||
*/
|
||||
class StatusPageNotification {
|
||||
/**
|
||||
* Find an SMTP notification config to use for sending.
|
||||
* Prefers the default notification if it is SMTP, otherwise
|
||||
* falls back to the first SMTP notification found.
|
||||
* @returns {Promise<object|null>} Parsed SMTP config or null
|
||||
*/
|
||||
static async getSMTPConfig() {
|
||||
try {
|
||||
let bean = await R.findOne("notification", " is_default = ? ", [true]);
|
||||
|
||||
if (bean) {
|
||||
let config = JSON.parse(bean.config);
|
||||
if (config.type === "smtp") {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
let allBeans = await R.findAll("notification");
|
||||
for (let b of allBeans) {
|
||||
let config = JSON.parse(b.config);
|
||||
if (config.type === "smtp") {
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
log.error("status-page-notification", `Failed to get SMTP config: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification email to the status page's configured address.
|
||||
* Does nothing if no notification_email is set or no SMTP config exists.
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {string} subject Email subject line
|
||||
* @param {string} body Plain text email body
|
||||
* @returns {Promise<boolean>} true if sent, false otherwise
|
||||
*/
|
||||
static async sendNotificationEmail(statusPageId, subject, body) {
|
||||
try {
|
||||
let statusPage = await R.load("status_page", statusPageId);
|
||||
if (!statusPage || !statusPage.notification_email) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let smtpConfig = await this.getSMTPConfig();
|
||||
if (!smtpConfig) {
|
||||
log.warn("status-page-notification", "No SMTP notification configured. Cannot send status page email.");
|
||||
return false;
|
||||
}
|
||||
|
||||
let sendConfig = Object.assign({}, smtpConfig, {
|
||||
smtpTo: statusPage.notification_email,
|
||||
});
|
||||
|
||||
await Notification.send(sendConfig, body);
|
||||
|
||||
log.info("status-page-notification", `Sent notification to ${statusPage.notification_email}: ${subject}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("status-page-notification", `Failed to send notification: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {object} incident Incident bean
|
||||
* @returns {Promise<boolean>} true if sent
|
||||
*/
|
||||
static async sendIncidentNotification(statusPageId, incident) {
|
||||
let subject = `[Incident] ${incident.title}`;
|
||||
let body = `Incident: ${incident.title}\nSeverity: ${incident.style}\n\n${incident.content}`;
|
||||
return await this.sendNotificationEmail(statusPageId, subject, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {object} incident Incident bean
|
||||
* @returns {Promise<boolean>} true if sent
|
||||
*/
|
||||
static async sendIncidentUpdateNotification(statusPageId, incident) {
|
||||
let subject = `[Incident Update] ${incident.title}`;
|
||||
let body = `Incident Updated: ${incident.title}\nSeverity: ${incident.style}\n\n${incident.content}`;
|
||||
return await this.sendNotificationEmail(statusPageId, subject, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {object} incident Incident bean
|
||||
* @returns {Promise<boolean>} true if sent
|
||||
*/
|
||||
static async sendIncidentResolvedNotification(statusPageId, incident) {
|
||||
let subject = `[Resolved] ${incident.title}`;
|
||||
let body = `Incident Resolved: ${incident.title}\n\nThis incident has been resolved.`;
|
||||
return await this.sendNotificationEmail(statusPageId, subject, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {object} maintenance Maintenance bean
|
||||
* @returns {Promise<boolean>} true if sent
|
||||
*/
|
||||
static async sendMaintenanceNotification(statusPageId, maintenance) {
|
||||
let subject = `[Maintenance] ${maintenance.title}`;
|
||||
let body = `Scheduled Maintenance: ${maintenance.title}\n\n${maintenance.description || ""}`;
|
||||
return await this.sendNotificationEmail(statusPageId, subject, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} statusPageId Status page ID
|
||||
* @param {object} maintenance Maintenance bean
|
||||
* @returns {Promise<boolean>} true if sent
|
||||
*/
|
||||
static async sendMaintenanceCompletedNotification(statusPageId, maintenance) {
|
||||
let subject = `[Maintenance Complete] ${maintenance.title}`;
|
||||
let body = `Maintenance Complete: ${maintenance.title}\n\nThe scheduled maintenance has ended.`;
|
||||
return await this.sendNotificationEmail(statusPageId, subject, body);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { StatusPageNotification };
|
||||
@ -0,0 +1,295 @@
|
||||
const { describe, test, mock, afterEach } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { R } = require("redbean-node");
|
||||
const { Notification } = require("../../server/notification");
|
||||
const { StatusPageNotification } = require("../../server/status-page-notification");
|
||||
|
||||
describe("StatusPageNotification", () => {
|
||||
afterEach(() => {
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
describe("getSMTPConfig()", () => {
|
||||
test("returns default notification if it is SMTP", async () => {
|
||||
let smtpConfig = { type: "smtp", smtpHost: "mail.example.com" };
|
||||
mock.method(R, "findOne", async () => ({
|
||||
config: JSON.stringify(smtpConfig),
|
||||
}));
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.deepStrictEqual(result, smtpConfig);
|
||||
});
|
||||
|
||||
test("falls back to first SMTP if default is not SMTP", async () => {
|
||||
let telegramConfig = { type: "telegram" };
|
||||
let smtpConfig = { type: "smtp", smtpHost: "mail.example.com" };
|
||||
|
||||
mock.method(R, "findOne", async () => ({
|
||||
config: JSON.stringify(telegramConfig),
|
||||
}));
|
||||
mock.method(R, "findAll", async () => [
|
||||
{ config: JSON.stringify(telegramConfig) },
|
||||
{ config: JSON.stringify(smtpConfig) },
|
||||
]);
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.deepStrictEqual(result, smtpConfig);
|
||||
});
|
||||
|
||||
test("falls back to first SMTP if no default notification", async () => {
|
||||
let smtpConfig = { type: "smtp", smtpHost: "mail.example.com" };
|
||||
|
||||
mock.method(R, "findOne", async () => null);
|
||||
mock.method(R, "findAll", async () => [{ config: JSON.stringify(smtpConfig) }]);
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.deepStrictEqual(result, smtpConfig);
|
||||
});
|
||||
|
||||
test("returns null if no SMTP notification exists", async () => {
|
||||
mock.method(R, "findOne", async () => null);
|
||||
mock.method(R, "findAll", async () => [
|
||||
{ config: JSON.stringify({ type: "telegram" }) },
|
||||
{ config: JSON.stringify({ type: "discord" }) },
|
||||
]);
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test("returns null if no notifications exist at all", async () => {
|
||||
mock.method(R, "findOne", async () => null);
|
||||
mock.method(R, "findAll", async () => []);
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
|
||||
test("returns null on database error", async () => {
|
||||
mock.method(R, "findOne", async () => {
|
||||
throw new Error("DB connection lost");
|
||||
});
|
||||
|
||||
let result = await StatusPageNotification.getSMTPConfig();
|
||||
assert.strictEqual(result, null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendNotificationEmail()", () => {
|
||||
test("returns false if status page has no notification_email", async () => {
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: null,
|
||||
}));
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
|
||||
test("returns false if status page has empty notification_email", async () => {
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: "",
|
||||
}));
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
|
||||
test("returns false if status page not found", async () => {
|
||||
mock.method(R, "load", async () => null);
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
|
||||
test("returns false if no SMTP config available", async () => {
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: "team@example.com",
|
||||
}));
|
||||
mock.method(StatusPageNotification, "getSMTPConfig", async () => null);
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
|
||||
test("sends email with overridden smtpTo and returns true", async () => {
|
||||
let smtpConfig = {
|
||||
type: "smtp",
|
||||
smtpHost: "mail.example.com",
|
||||
smtpTo: "original@example.com",
|
||||
};
|
||||
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: "team@example.com",
|
||||
}));
|
||||
mock.method(StatusPageNotification, "getSMTPConfig", async () => smtpConfig);
|
||||
|
||||
let sentConfig = null;
|
||||
mock.method(Notification, "send", async (config, msg) => {
|
||||
sentConfig = config;
|
||||
return "Sent Successfully.";
|
||||
});
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Test Subject", "Test Body");
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
assert.strictEqual(sentConfig.smtpTo, "team@example.com");
|
||||
assert.strictEqual(sentConfig.smtpHost, "mail.example.com");
|
||||
assert.strictEqual(sentConfig.type, "smtp");
|
||||
});
|
||||
|
||||
test("does not mutate original SMTP config", async () => {
|
||||
let smtpConfig = {
|
||||
type: "smtp",
|
||||
smtpHost: "mail.example.com",
|
||||
smtpTo: "original@example.com",
|
||||
};
|
||||
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: "team@example.com",
|
||||
}));
|
||||
mock.method(StatusPageNotification, "getSMTPConfig", async () => smtpConfig);
|
||||
mock.method(Notification, "send", async () => "Sent Successfully.");
|
||||
|
||||
await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
|
||||
assert.strictEqual(smtpConfig.smtpTo, "original@example.com");
|
||||
});
|
||||
|
||||
test("returns false on send failure", async () => {
|
||||
mock.method(R, "load", async () => ({
|
||||
notification_email: "team@example.com",
|
||||
}));
|
||||
mock.method(StatusPageNotification, "getSMTPConfig", async () => ({
|
||||
type: "smtp",
|
||||
smtpHost: "mail.example.com",
|
||||
}));
|
||||
mock.method(Notification, "send", async () => {
|
||||
throw new Error("SMTP connection refused");
|
||||
});
|
||||
|
||||
let result = await StatusPageNotification.sendNotificationEmail(1, "Subject", "Body");
|
||||
assert.strictEqual(result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendIncidentNotification()", () => {
|
||||
test("sends with correct subject and body", async () => {
|
||||
let sentSubject = null;
|
||||
let sentBody = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentSubject = subject;
|
||||
sentBody = body;
|
||||
return true;
|
||||
});
|
||||
|
||||
let incident = {
|
||||
title: "API Outage",
|
||||
content: "The API is currently unavailable.",
|
||||
style: "danger",
|
||||
};
|
||||
|
||||
let result = await StatusPageNotification.sendIncidentNotification(5, incident);
|
||||
|
||||
assert.strictEqual(result, true);
|
||||
assert.strictEqual(sentSubject, "[Incident] API Outage");
|
||||
assert.ok(sentBody.includes("API Outage"));
|
||||
assert.ok(sentBody.includes("danger"));
|
||||
assert.ok(sentBody.includes("The API is currently unavailable."));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendIncidentUpdateNotification()", () => {
|
||||
test("sends with correct subject", async () => {
|
||||
let sentSubject = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentSubject = subject;
|
||||
return true;
|
||||
});
|
||||
|
||||
let incident = {
|
||||
title: "API Outage",
|
||||
content: "Investigating the issue.",
|
||||
style: "warning",
|
||||
};
|
||||
|
||||
await StatusPageNotification.sendIncidentUpdateNotification(5, incident);
|
||||
assert.strictEqual(sentSubject, "[Incident Update] API Outage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendIncidentResolvedNotification()", () => {
|
||||
test("sends with correct subject and resolved message", async () => {
|
||||
let sentSubject = null;
|
||||
let sentBody = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentSubject = subject;
|
||||
sentBody = body;
|
||||
return true;
|
||||
});
|
||||
|
||||
let incident = { title: "API Outage" };
|
||||
|
||||
await StatusPageNotification.sendIncidentResolvedNotification(5, incident);
|
||||
assert.strictEqual(sentSubject, "[Resolved] API Outage");
|
||||
assert.ok(sentBody.includes("resolved"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMaintenanceNotification()", () => {
|
||||
test("sends with correct subject and body", async () => {
|
||||
let sentSubject = null;
|
||||
let sentBody = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentSubject = subject;
|
||||
sentBody = body;
|
||||
return true;
|
||||
});
|
||||
|
||||
let maintenance = {
|
||||
title: "Database Upgrade",
|
||||
description: "Upgrading to PostgreSQL 16.",
|
||||
};
|
||||
|
||||
await StatusPageNotification.sendMaintenanceNotification(3, maintenance);
|
||||
assert.strictEqual(sentSubject, "[Maintenance] Database Upgrade");
|
||||
assert.ok(sentBody.includes("Upgrading to PostgreSQL 16."));
|
||||
});
|
||||
|
||||
test("handles null description gracefully", async () => {
|
||||
let sentBody = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentBody = body;
|
||||
return true;
|
||||
});
|
||||
|
||||
let maintenance = {
|
||||
title: "Quick Fix",
|
||||
description: null,
|
||||
};
|
||||
|
||||
await StatusPageNotification.sendMaintenanceNotification(3, maintenance);
|
||||
assert.ok(sentBody.includes("Quick Fix"));
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMaintenanceCompletedNotification()", () => {
|
||||
test("sends with correct subject", async () => {
|
||||
let sentSubject = null;
|
||||
|
||||
mock.method(StatusPageNotification, "sendNotificationEmail", async (id, subject, body) => {
|
||||
sentSubject = subject;
|
||||
return true;
|
||||
});
|
||||
|
||||
let maintenance = { title: "Database Upgrade" };
|
||||
|
||||
await StatusPageNotification.sendMaintenanceCompletedNotification(3, maintenance);
|
||||
assert.strictEqual(sentSubject, "[Maintenance Complete] Database Upgrade");
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in new issue