From ce625a7874c951bbdd2a1b6cd1a4f5e1fb538360 Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Wed, 22 Apr 2026 18:07:52 -0400 Subject: [PATCH] fix(tests): patch hate_crack.main._notify directly tests/test_random_rules_attack.py purges and re-imports hate_crack.* modules, which leaves main._notify pointing at a different notify object than a top-level patch("hate_crack.notify._send_pushover") would touch. Under the full suite that caused the test's mock to miss and the production call to hit the real Pushover API. Switch to patch.object(hc_main, "_notify") -- same pattern as tests/test_run_hcat_cmd.py but anchored to the exact module object already bound as hc_main, so it is immune to sys.modules churn regardless of import order. Drop the now-redundant _install_settings helper and _reset_notify_state fixture. --- tests/test_ui_test_pushover.py | 80 +++++++++++++++++----------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/tests/test_ui_test_pushover.py b/tests/test_ui_test_pushover.py index 069faee..f5b43b4 100644 --- a/tests/test_ui_test_pushover.py +++ b/tests/test_ui_test_pushover.py @@ -1,41 +1,41 @@ -"""Unit tests for main.test_pushover_notification().""" +"""Unit tests for main.test_pushover_notification(). + +These tests patch ``hate_crack.main._notify`` directly rather than +``hate_crack.notify._send_pushover``. The latter is fragile because +``tests/test_random_rules_attack.py`` purges ``hate_crack.*`` from +``sys.modules`` and re-imports, which leaves ``main._notify`` pointing +at a different module object than the one a top-level ``patch`` would +touch. Patching the attribute on ``main`` itself is robust to that. + +We use ``patch.object(hc_main, "_notify")`` rather than +``patch("hate_crack.main._notify")`` so the patch target is the exact +module object whose function we invoke. A string target would re-resolve +``hate_crack.main`` through ``sys.modules``, which — again thanks to the +purge in ``test_random_rules_attack.py`` — can be a different object from +the ``hc_main`` reference bound at test-module import time. +""" + +from types import SimpleNamespace from unittest.mock import patch -import pytest - from hate_crack import main as hc_main -from hate_crack import notify -from hate_crack.notify.settings import NotifySettings -@pytest.fixture(autouse=True) -def _reset_notify_state(): - notify.clear_state_for_tests() - yield - notify.clear_state_for_tests() - - -def _install_settings( - *, - enabled: bool = True, - token: str = "tok", - user: str = "usr", -) -> None: - """Swap fresh settings into the notify module for this test.""" - settings = NotifySettings() - settings.enabled = enabled - settings.pushover_token = token - settings.pushover_user = user - notify._settings = settings +def _settings( + *, enabled: bool = True, token: str = "tok", user: str = "usr" +) -> SimpleNamespace: + """Minimal stand-in for ``NotifySettings`` — we only read three fields.""" + return SimpleNamespace(enabled=enabled, pushover_token=token, pushover_user=user) class TestTestPushoverNotification: def test_success_prints_confirmation_and_sends(self, capsys): - _install_settings(enabled=True, token="tok", user="usr") - with patch("hate_crack.notify._send_pushover", return_value=True) as send: + with patch.object(hc_main, "_notify") as mock_notify: + mock_notify.get_settings.return_value = _settings(enabled=True) + mock_notify._send_pushover.return_value = True hc_main.test_pushover_notification() - assert send.called - args = send.call_args.args + mock_notify._send_pushover.assert_called_once() + args = mock_notify._send_pushover.call_args.args assert args[0] == "tok" assert args[1] == "usr" assert args[2] == "hate_crack: test notification" @@ -44,34 +44,36 @@ class TestTestPushoverNotification: assert "[+] Test Pushover notification sent" in out def test_failure_prints_failure_line(self, capsys): - _install_settings(enabled=True, token="tok", user="usr") - with patch("hate_crack.notify._send_pushover", return_value=False): + with patch.object(hc_main, "_notify") as mock_notify: + mock_notify.get_settings.return_value = _settings(enabled=True) + mock_notify._send_pushover.return_value = False hc_main.test_pushover_notification() out = capsys.readouterr().out assert "[!] Test Pushover notification failed" in out def test_missing_token_skips_send_and_warns(self, capsys): - _install_settings(enabled=True, token="", user="usr") - with patch("hate_crack.notify._send_pushover") as send: + with patch.object(hc_main, "_notify") as mock_notify: + mock_notify.get_settings.return_value = _settings(enabled=True, token="") hc_main.test_pushover_notification() - send.assert_not_called() + mock_notify._send_pushover.assert_not_called() out = capsys.readouterr().out assert "[!] Pushover credentials missing" in out assert "notify_pushover_token" in out def test_missing_user_skips_send_and_warns(self, capsys): - _install_settings(enabled=True, token="tok", user="") - with patch("hate_crack.notify._send_pushover") as send: + with patch.object(hc_main, "_notify") as mock_notify: + mock_notify.get_settings.return_value = _settings(enabled=True, user="") hc_main.test_pushover_notification() - send.assert_not_called() + mock_notify._send_pushover.assert_not_called() out = capsys.readouterr().out assert "[!] Pushover credentials missing" in out def test_globally_off_still_sends_with_note(self, capsys): - _install_settings(enabled=False, token="tok", user="usr") - with patch("hate_crack.notify._send_pushover", return_value=True) as send: + with patch.object(hc_main, "_notify") as mock_notify: + mock_notify.get_settings.return_value = _settings(enabled=False) + mock_notify._send_pushover.return_value = True hc_main.test_pushover_notification() - send.assert_called_once() + mock_notify._send_pushover.assert_called_once() out = capsys.readouterr().out assert "notifications are globally OFF" in out assert "[+] Test Pushover notification sent" in out