test_finish_photo_link_auth.py 3.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """The {finish_photo_url} link opens with authentication on too.
  2. The archive photo route needs a media token when authentication is on, and
  3. nothing tapping a link in Telegram, CallMeBot or a Home Assistant notification
  4. has one, so the link answered 401. With authentication on, the link now points
  5. at a notification photo instead: an unguessable name that opens that one photo
  6. for 3 days. With authentication off it stays the archive link, which needs no
  7. login and doesn't expire.
  8. """
  9. from types import SimpleNamespace
  10. from unittest.mock import AsyncMock, patch
  11. import pytest
  12. from backend.app.utils import notification_photos
  13. ARCHIVE_ID = 42
  14. FILENAME = "finish_20260101_120000_abcd1234.jpg"
  15. @pytest.fixture
  16. def photo(tmp_path, monkeypatch):
  17. """A finish photo on disk, and the notification photo store under tmp_path."""
  18. monkeypatch.setattr(notification_photos.settings, "base_dir", tmp_path)
  19. path = tmp_path / FILENAME
  20. path.write_bytes(b"\xff\xd8finish-photo")
  21. return path
  22. async def _call(*, auth_on, external_url="https://bambuddy.example", photo_path=None, auth_error=False):
  23. from backend.app.main import _finish_photo_for_notification
  24. auth = AsyncMock(side_effect=RuntimeError("db down")) if auth_error else AsyncMock(return_value=auth_on)
  25. with (
  26. patch("backend.app.api.routes.settings.get_setting", AsyncMock(return_value=external_url)),
  27. patch("backend.app.core.auth.is_auth_enabled", auth),
  28. patch("backend.app.utils.archive_paths.find_archive_photo", return_value=photo_path),
  29. ):
  30. return await _finish_photo_for_notification(AsyncMock(), SimpleNamespace(), ARCHIVE_ID, FILENAME)
  31. class TestFinishPhotoLink:
  32. @pytest.mark.asyncio
  33. async def test_auth_off_keeps_the_archive_link(self, photo):
  34. url, data = await _call(auth_on=False, photo_path=photo)
  35. assert url == f"https://bambuddy.example/api/v1/archives/{ARCHIVE_ID}/photos/{FILENAME}"
  36. assert data == photo.read_bytes()
  37. @pytest.mark.asyncio
  38. async def test_auth_off_without_external_url_is_relative(self, photo):
  39. url, _ = await _call(auth_on=False, external_url=None, photo_path=photo)
  40. assert url == f"/api/v1/archives/{ARCHIVE_ID}/photos/{FILENAME}"
  41. @pytest.mark.asyncio
  42. async def test_auth_on_links_a_notification_photo_that_serves_the_same_bytes(self, photo):
  43. url, data = await _call(auth_on=True, photo_path=photo)
  44. prefix = "https://bambuddy.example/api/v1/notifications/photos/"
  45. assert url.startswith(prefix)
  46. served = notification_photos.find_notification_photo(url[len(prefix) :])
  47. assert served is not None
  48. assert served.read_bytes() == photo.read_bytes()
  49. assert data == photo.read_bytes()
  50. @pytest.mark.asyncio
  51. async def test_auth_check_failing_is_treated_as_auth_on(self, photo):
  52. url, _ = await _call(auth_on=False, auth_error=True, photo_path=photo)
  53. assert "/api/v1/notifications/photos/" in url
  54. @pytest.mark.asyncio
  55. async def test_auth_on_without_the_photo_gives_no_link(self, photo):
  56. # A link to the archive route would only answer 401, so none at all.
  57. assert await _call(auth_on=True, photo_path=None) == (None, None)
  58. @pytest.mark.asyncio
  59. async def test_oversized_photo_is_linked_but_not_attached(self, photo):
  60. photo.write_bytes(b"\xff" * 2_500_001)
  61. url, data = await _call(auth_on=True, photo_path=photo)
  62. assert url is not None
  63. assert data is None