uptime-kuma/test/backend-test/test-database-down-notification.js

202 lines
8.6 KiB
JavaScript

const { describe, test, before, after } = require("node:test");
const assert = require("node:assert");
const { R } = require("redbean-node");
const { Notification } = require("../../server/notification");
const Database = require("../../server/database");
describe("Database Down Notification", () => {
let testNotification;
before(async () => {
// Initialize data directory first (required before connecting)
Database.initDataDir({});
// Ensure database is connected (this copies template DB which has basic tables)
await Database.connect(true); // testMode = true
// Ensure notification table exists with all required columns
// The template DB might be outdated, so we need to ensure schema is complete
const hasNotificationTable = await R.hasTable("notification");
if (!hasNotificationTable) {
// If table doesn't exist, create all tables first
const { createTables } = require("../../db/knex_init_db.js");
await createTables();
} else {
// Table exists, but check if it has all required columns (template DB might be outdated)
// Add missing columns if they don't exist
const hasIsDefault = await R.knex.schema.hasColumn("notification", "is_default");
if (!hasIsDefault) {
await R.knex.schema.alterTable("notification", (table) => {
table.boolean("is_default").notNullable().defaultTo(false);
});
}
}
// Run migrations to ensure schema is current (including send_database_down column)
// Database.patch() handles migrations properly with foreign key checks
try {
await Database.patch(undefined, undefined);
} catch (e) {
// Some migrations may fail if tables don't exist in template DB, that's okay
// But we still need to ensure our column exists, so add it manually if migration failed
if (!e.message.includes("the following files are missing:")) {
console.warn("Migration warning (may be expected):", e.message);
// Fallback: ensure the column exists if migration didn't complete
const hasColumn = await R.knex.schema.hasColumn("notification", "send_database_down");
if (!hasColumn) {
await R.knex.schema.alterTable("notification", (table) => {
table.boolean("send_database_down").notNullable().defaultTo(false);
});
}
}
}
// Create a test notification with send_database_down enabled (opt-in)
const notificationBean = R.dispense("notification");
notificationBean.name = "Test Notification";
notificationBean.user_id = 1;
notificationBean.config = JSON.stringify({
type: "webhook",
webhookURL: "https://example.com/webhook",
});
notificationBean.active = 1;
notificationBean.is_default = 0;
notificationBean.send_database_down = 1; // Opt-in for database down notifications
await R.store(notificationBean);
testNotification = notificationBean;
});
after(async () => {
// Clean up test notification
if (testNotification) {
try {
await R.trash(testNotification);
} catch (e) {
// Ignore cleanup errors
}
}
await Database.close();
});
test("refreshCache() loads only opt-in notifications into cache", async () => {
// Create a notification that is NOT opted-in
const nonOptInBean = R.dispense("notification");
nonOptInBean.name = "Non-Opt-In Notification";
nonOptInBean.user_id = 1;
nonOptInBean.config = JSON.stringify({ type: "webhook", webhookURL: "https://example.com/webhook2" });
nonOptInBean.active = 1;
nonOptInBean.is_default = 0;
nonOptInBean.send_database_down = 0; // NOT opted-in
await R.store(nonOptInBean);
try {
await Notification.refreshCache();
assert.ok(Notification.notificationCache.length > 0, "Cache should contain notifications");
assert.ok(Notification.cacheLastRefresh > 0, "Cache refresh time should be set");
// Verify test notification (opt-in) is in cache
const cached = Notification.notificationCache.find((n) => n.id === testNotification.id);
assert.ok(cached, "Opt-in notification should be in cache");
assert.strictEqual(cached.name, "Test Notification");
// Config is stored as raw string, parse to verify
const config = JSON.parse(cached.config);
assert.strictEqual(config.type, "webhook");
// Verify non-opt-in notification is NOT in cache
const nonOptInCached = Notification.notificationCache.find((n) => n.id === nonOptInBean.id);
assert.strictEqual(nonOptInCached, undefined, "Non-opt-in notification should NOT be in cache");
} finally {
// Clean up
await R.trash(nonOptInBean);
}
});
test("sendDatabaseDownNotification() uses cached notifications and prevents duplicates", async () => {
// Ensure cache is populated
await Notification.refreshCache();
assert.ok(Notification.notificationCache.length > 0, "Cache should be populated");
// Reset the flag
Notification.resetDatabaseDownFlag();
assert.strictEqual(Notification.databaseDownNotificationSent, false);
// Mock the send method to track calls
let sendCallCount = 0;
const originalSend = Notification.send;
Notification.send = async (notification, msg) => {
sendCallCount++;
assert.ok(msg.includes("Database Connection Failed"), "Message should mention database failure");
return "OK";
};
try {
// First call should send
await Notification.sendDatabaseDownNotification("Test database error: ECONNREFUSED");
assert.ok(sendCallCount > 0, "send() should have been called");
assert.strictEqual(Notification.databaseDownNotificationSent, true, "Flag should be set");
const firstCallCount = sendCallCount;
// Second call should not send again (duplicate prevention)
await Notification.sendDatabaseDownNotification("Test error 2");
assert.strictEqual(sendCallCount, firstCallCount, "Should not send again on second call");
} finally {
// Restore original send method
Notification.send = originalSend;
}
});
test("sendDatabaseDownNotification() handles empty cache gracefully", async () => {
// Clear cache
Notification.notificationCache = [];
Notification.cacheLastRefresh = 0;
Notification.resetDatabaseDownFlag();
// Should not throw
await Notification.sendDatabaseDownNotification("Test error");
// Flag should remain false since cache is empty
assert.strictEqual(Notification.databaseDownNotificationSent, false);
});
test("resetDatabaseDownFlag() resets the notification flag", () => {
Notification.databaseDownNotificationSent = true;
Notification.resetDatabaseDownFlag();
assert.strictEqual(Notification.databaseDownNotificationSent, false);
});
test("refreshCache() handles database errors gracefully", async () => {
// Ensure cache is populated first
await Notification.refreshCache();
const originalCacheLength = Notification.notificationCache.length;
const originalCacheItems = JSON.parse(JSON.stringify(Notification.notificationCache)); // Deep copy
// Temporarily mock R.find to throw an error
const originalFind = R.find;
R.find = async () => {
throw new Error("Database connection lost");
};
try {
// Should not throw
await Notification.refreshCache();
// Cache should remain unchanged (not cleared)
assert.ok(Array.isArray(Notification.notificationCache), "Cache should still be an array");
assert.strictEqual(
Notification.notificationCache.length,
originalCacheLength,
"Cache should have same length"
);
assert.deepStrictEqual(
Notification.notificationCache,
originalCacheItems,
"Cache should contain same items"
);
} finally {
R.find = originalFind;
}
});
});