diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fc9edac2b..0bf4b8413 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -131,6 +131,13 @@ set(base_test_SOURCES $ ) +if(ICINGA2_WITH_NOTIFICATION) + list(APPEND base_test_SOURCES + notification-notificationcomponent.cpp + $ + ) +endif() + if(ICINGA2_UNITY_BUILD) mkunity_target(base test base_test_SOURCES) endif() diff --git a/test/notification-notificationcomponent.cpp b/test/notification-notificationcomponent.cpp new file mode 100644 index 000000000..880ef9252 --- /dev/null +++ b/test/notification-notificationcomponent.cpp @@ -0,0 +1,506 @@ +/* Icinga 2 | (c) 2025 Icinga GmbH | GPLv2+ */ + +#include +#include "base/defer.hpp" +#include "remote/apilistener.hpp" +#include "test/base-testloggerfixture.hpp" +#include "config/configcompiler.hpp" +#include "notification/notificationcomponent.hpp" + +using namespace icinga; + +namespace { + +/** + * Gets the pointer to the private NotificationTimerHandler() by using Friend-Injection. + * + * This uses the exception the standard makes to private member access for explicit template + * instantiation by instantiating a type that defines an accessor to the member function pointer + * in the surrounding anonymous namespace. + * + * The reason for the anonymous namespace is that it doesn't violate the ODR if other + * instantiations are made to this template in other translation units. + * This isn't actually an issue here because the name is very specific to the single use-case of + * obtaining access to NotificationTimerHandler(). + */ +template +struct InvokeTimerHandlerImpl +{ + friend void InvokeTimerHandler(const NotificationComponent::Ptr& nc) { (*nc.*privateMemberFnPtr)(); } +}; +void InvokeTimerHandler(const NotificationComponent::Ptr& nc); + +template struct InvokeTimerHandlerImpl<&NotificationComponent::NotificationTimerHandler>; + +} // namespace + +class NotificationComponentFixture : public TestLoggerFixture +{ +public: + NotificationComponentFixture() + { + auto createObjects = []() { + String config = R"CONFIG({ +object CheckCommand "dummy" { + command = "/bin/echo" +} +object Host "h1" { + address = "h1" + check_command = "dummy" + enable_notifications = true + enable_active_checks = false + enable_passive_checks = true +} +object NotificationCommand "send" { + command = ["true"] +} +apply Notification "n1" to Host { + interval = 0 + command = "send" + period = "tp1" + users = [ "u1" ] + assign where host.enable_notifications == true +} +object User "u1" { + enable_notifications = true +} +object TimePeriod "tp1" { + display_name = "Test TimePeriod" + ranges = { + "monday" = "00:00-24:00" + "tuesday" = "00:00-24:00" + "wednesday" = "00:00-24:00" + "thursday" = "00:00-24:00" + "friday" = "00:00-24:00" + "saturday" = "00:00-24:00" + "sunday" = "00:00-24:00" + } +} +object NotificationComponent "nc" {} +})CONFIG"; + std::unique_ptr expr = ConfigCompiler::CompileText("", config); + expr->Evaluate(*ScriptFrame::GetCurrentFrame()); + }; + + auto ret = ConfigItem::RunWithActivationContext(new Function("CreateTestObjects", createObjects)); + BOOST_REQUIRE(ret); + + m_Host = Host::GetByName("h1"); + BOOST_REQUIRE(m_Host); + + m_Notification = Notification::GetByName("h1!n1"); + BOOST_REQUIRE(m_Notification); + + m_TimePeriod = TimePeriod::GetByName("tp1"); + BOOST_REQUIRE(m_TimePeriod); + + ApiListener::UpdateObjectAuthority(); + BOOST_REQUIRE(ApiListener::UpdatedObjectAuthority()); + + // Store the old periods from the config snippets to reuse them later. + m_AllTimePeriod = m_TimePeriod->GetRanges(); + + Checkable::OnNotificationSentToUser.connect(NotificationSentToUserHandler); + } + + static void NotificationSentToUserHandler( + const Notification::Ptr&, + const Checkable::Ptr&, + const User::Ptr&, + const NotificationType& type, + const CheckResult::Ptr&, + const String&, + const String&, + const String&, + const MessageOrigin::Ptr& + ) + { + std::unique_lock lock(m_NotificationMutex); + m_LastNotification = type; + m_NumNotifications++; + lock.unlock(); + m_NotificationCv.notify_all(); + } + + bool WaitForExpectedNotificationCount(std::size_t expectedCount, std::chrono::milliseconds timeout = 5s) + { + Defer clearLog{[this]() { ClearTestLogger(); }}; + std::unique_lock lock(m_NotificationMutex); + if (m_NumNotifications > expectedCount) { + return false; + } + return m_NotificationCv.wait_for(lock, timeout, [&]() { return m_NumNotifications == expectedCount; }); + } + + boost::test_tools::assertion_result AssertNoAttemptedSendLogPattern() + { + auto result = ExpectLogPattern("^(Sending|Attempting to (re-)?send).*?notification.*$", 0s); + ClearTestLogger(); + return !result; + } + + void BeginTimePeriod() + { + ObjectLock lock{m_TimePeriod}; + m_TimePeriod->SetRanges(m_AllTimePeriod); + + auto now = Utility::GetTime(); + m_TimePeriod->UpdateRegion(now, now + 1e3, true); + BOOST_REQUIRE(m_TimePeriod->IsInside(now)); + } + + void EndTimePeriod() + { + ObjectLock lock{m_TimePeriod}; + m_TimePeriod->SetRanges(new Dictionary); + + auto now = Utility::GetTime(); + m_TimePeriod->UpdateRegion(now, now + 1e3, true); + BOOST_REQUIRE(!m_TimePeriod->IsInside(now)); + } + + void SetNotificationInverval(double interval) { m_Notification->SetInterval(interval); } + + void WaitUntilNextReminderScheduled() + { + Utility::Sleep(m_Notification->GetNextNotification() - Utility::GetTime() + 0.01); + BOOST_REQUIRE_LE(m_Notification->GetNextNotification(), Utility::GetTime()); + } + + static void NotificationTimerHandler() + { + auto nc = NotificationComponent::GetByName("nc"); + InvokeTimerHandler(nc); + } + + void ReceiveCheckResults(std::size_t num, ServiceState state) + { + StoppableWaitGroup::Ptr wg = new StoppableWaitGroup(); + + for (auto i = 0UL; i < num; ++i) { + CheckResult::Ptr cr = new CheckResult(); + + cr->SetState(state); + + double now = Utility::GetTime(); + cr->SetActive(false); + cr->SetScheduleStart(now); + cr->SetScheduleEnd(now); + cr->SetExecutionStart(now); + cr->SetExecutionEnd(now); + + BOOST_REQUIRE(m_Host->ProcessCheckResult(cr, wg) == Checkable::ProcessingResult::Ok); + } + } + + double GetLastNotificationTimestamp() { return m_Notification->GetLastNotification(); } + std::uint8_t GetSuppressedNotifications() { return m_Notification->GetSuppressedNotifications(); } + static NotificationType GetLastNotification() { return m_LastNotification; } + static std::size_t GetNotificationCount() { return m_NumNotifications; } + +private: + static inline std::mutex m_NotificationMutex; + static inline std::condition_variable m_NotificationCv; + static inline std::size_t m_NumNotifications{}; + static inline NotificationType m_LastNotification{}; + + Host::Ptr m_Host; + Notification::Ptr m_Notification; + TimePeriod::Ptr m_TimePeriod; + Dictionary::Ptr m_AllTimePeriod; +}; + +BOOST_FIXTURE_TEST_SUITE(notificationcomponent, NotificationComponentFixture); + +/* Test sending out reminder notifications in a given interval. + */ +BOOST_AUTO_TEST_CASE(notify_send_reminders) +{ + SetNotificationInverval(0.15); + ReceiveCheckResults(2, ServiceCritical); + + // The first run of the timer sets up the next reminder notification. + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(1)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + + // Rerunning the timer before the next interval should not trigger a reminder notification. + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + + // After waiting until the interval has passed, a reminder will be queued. + WaitUntilNextReminderScheduled(); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(2)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + + // Now we test that reminders are only sent for Critical states. + // Hard state is switched to OK. + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(3)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationRecovery); + + // Now we wait for one interval and check that no reminder has been sent. + WaitUntilNextReminderScheduled(); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 3); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationRecovery); +} + +/* Tests simple sending of notifications on each state change. + */ +BOOST_AUTO_TEST_CASE(notify_simple) +{ + BeginTimePeriod(); + + ReceiveCheckResults(2, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(1)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + + ReceiveCheckResults(1, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(2)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationRecovery); + + ReceiveCheckResults(2, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 2); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationRecovery); +} + +/* This tests the simplest case where a suppressed notification will be sent after resuming + * a TimePeriod. A single event occurs outside the TimePeriod and the notification should be + * sent as soon as the timer runs after the TimePeriod is resumed. + */ +BOOST_AUTO_TEST_CASE(notify_after_timeperiod_simple) +{ + BeginTimePeriod(); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + EndTimePeriod(); + + ReceiveCheckResults(3, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + BeginTimePeriod(); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(1)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); +} + +/* Similar to the test-case above, but has multiple state-changes outside of the TimePeriod + * This is important, since there are multiple places in the code that check on and make modifications + * to the list of suppressed events. A concrete example of a bug like this is #10575. + */ +BOOST_AUTO_TEST_CASE(notify_multiple_state_changes_outside_timeperiod) +{ + BOOST_REQUIRE_EQUAL(GetLastNotificationTimestamp(), 0.0); + + BeginTimePeriod(); + ReceiveCheckResults(2, ServiceCritical); + + BOOST_REQUIRE(WaitForExpectedNotificationCount(1)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + EndTimePeriod(); + + ReceiveCheckResults(1, ServiceOK); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationRecovery); + + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationRecovery); + + ReceiveCheckResults(1, ServiceCritical); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationRecovery); + + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + // NOTE: This is the bug. The timer run above has reset the suppressed ServiceOK event + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + // Third Critical check result will set the Critical hard state. + ReceiveCheckResults(2, ServiceCritical); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + ReceiveCheckResults(1, ServiceOK); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + // Resonably we would now expect a NotificationRecovered, but it will not arrive due to the bug. + BeginTimePeriod(); + + // No notification will be received because the suppressed notifications got borked + // when they were cleared previously in FireSuppressedNotifications() + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); +} + +/* This tests if suppressed notifications of opposite types cancel each other out. + */ +BOOST_AUTO_TEST_CASE(no_notify_suppressed_cancel_out) +{ + BOOST_REQUIRE_EQUAL(GetLastNotificationTimestamp(), 0.0); + + BeginTimePeriod(); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + EndTimePeriod(); + + ReceiveCheckResults(3, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + BeginTimePeriod(); + + // Ensure no notification is sent after resuming the TimePeriod. + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + // Now repeat the same starting from a Critical state + ReceiveCheckResults(3, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(WaitForExpectedNotificationCount(1)); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + EndTimePeriod(); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationRecovery); + + ReceiveCheckResults(3, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + BeginTimePeriod(); + + // Ensure no notification is sent after resuming the TimePeriod. + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 1); + BOOST_REQUIRE_EQUAL(GetLastNotification(), NotificationProblem); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); +} + +/* This may look similar to the test-case above, but the critical difference is that here + * the final state change happens inside the TimePeriod again, but before the timer runs. + * The outdated suppressed NotificationProblem will then be subtracted when the timer runs. + */ +BOOST_AUTO_TEST_CASE(no_notify_non_applicable_reason) +{ + BOOST_REQUIRE_EQUAL(GetLastNotificationTimestamp(), 0.0); + + BeginTimePeriod(); + + ReceiveCheckResults(1, ServiceOK); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); + + EndTimePeriod(); + + // We queue a suppressed notification. + ReceiveCheckResults(3, ServiceCritical); + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + BeginTimePeriod(); + + // In this scenario a check result that goes against the suppressed notification is processed + // before the timer can run again. No notification should be sent, because the last state + // change the user was notified about was the same. + ReceiveCheckResults(1, ServiceOK); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), NotificationProblem); + + // When the timer runs, it should clear the suppressed notification but not send anything. + NotificationTimerHandler(); + BOOST_REQUIRE(AssertNoAttemptedSendLogPattern()); + BOOST_REQUIRE_EQUAL(GetNotificationCount(), 0); + BOOST_REQUIRE_EQUAL(GetLastNotification(), 0); + BOOST_REQUIRE_EQUAL(GetSuppressedNotifications(), 0); +} + +BOOST_AUTO_TEST_SUITE_END()