test_notification_refused_header.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. """A refused header's value must not reach a notification's stored error.
  2. A Home Assistant token with a stray space makes httpx refuse the header and
  3. quote its value. The send's error text went into the provider's last_error and
  4. the notification log, which notification readers (Viewers, by default) read
  5. through the API, and back to whoever ran a test notification. The token is an
  6. admin-only setting.
  7. """
  8. from __future__ import annotations
  9. import socket
  10. import pytest
  11. from sqlalchemy import select
  12. from backend.app.models.notification import NotificationLog, NotificationProvider
  13. from backend.app.services.notification_service import NotificationService
  14. # Made up. Never paste a value from a real log here.
  15. SECRET = "ha-notify-value-7"
  16. @pytest.fixture
  17. def home_assistant(monkeypatch):
  18. """HA settings from the environment, pointed at a socket that accepts."""
  19. server = socket.socket()
  20. server.bind(("127.0.0.1", 0))
  21. server.listen()
  22. monkeypatch.setenv("HA_URL", f"http://127.0.0.1:{server.getsockname()[1]}")
  23. monkeypatch.setenv("HA_TOKEN", f"{SECRET} ")
  24. yield
  25. server.close()
  26. @pytest.mark.asyncio
  27. async def test_a_failed_send_stores_no_header_value(db_session, home_assistant):
  28. provider = NotificationProvider(
  29. name="HA", provider_type="homeassistant", enabled=True, config="{}", attach_photo=False
  30. )
  31. db_session.add(provider)
  32. await db_session.commit()
  33. await NotificationService()._send_to_providers([provider], "Title", "Body", db_session, event_type="test")
  34. await db_session.refresh(provider)
  35. log = (await db_session.execute(select(NotificationLog))).scalar_one()
  36. assert provider.last_error and "Illegal header value [REDACTED]" in provider.last_error
  37. assert SECRET not in provider.last_error
  38. assert log.error_message and SECRET not in log.error_message
  39. @pytest.mark.asyncio
  40. async def test_a_test_notification_returns_no_header_value(db_session, home_assistant):
  41. success, message = await NotificationService().send_test_notification(
  42. "homeassistant", {}, db=db_session, attach_photo=False
  43. )
  44. assert success is False
  45. assert "Illegal header value [REDACTED]" in message
  46. assert SECRET not in message
  47. @pytest.mark.asyncio
  48. async def test_an_error_raised_out_of_a_send_is_stored_masked(db_session):
  49. """The dispatcher's own except stores str(e) as well."""
  50. provider = NotificationProvider(name="X", provider_type="webhook", enabled=True, config="{}")
  51. db_session.add(provider)
  52. await db_session.commit()
  53. service = NotificationService()
  54. async def _raise(*_args, **_kwargs):
  55. raise RuntimeError(f"Illegal header value b'Bearer {SECRET} '")
  56. service._send_to_provider = _raise
  57. await service._send_to_providers([provider], "Title", "Body", db_session, event_type="test")
  58. await db_session.refresh(provider)
  59. log = (await db_session.execute(select(NotificationLog))).scalar_one()
  60. assert SECRET not in (provider.last_error or "") and "[REDACTED]" in provider.last_error
  61. assert SECRET not in (log.error_message or "")