test_announcements_api.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. """Who sees announcements, and per-user read state.
  2. Administrators see them; other signed-in users only when
  3. ``announcements_all_users`` is on. With authentication off, the person running
  4. Bambuddy sees them. Anyone else gets ``visible: false`` and an empty list. Those
  5. who may see them get ``visible: true`` even with nothing published, because the
  6. sidebar entry is there for them either way.
  7. """
  8. import json
  9. import secrets
  10. import pytest
  11. from httpx import AsyncClient
  12. from backend.app.models.announcement import Announcement
  13. from backend.app.models.settings import Settings
  14. URL = "/api/v1/announcements"
  15. _FIXTURE_PW = "Aa1!" + secrets.token_urlsafe(12) # pragma: allowlist secret
  16. async def _seed(db, *public_ids: str, level: str = "important") -> None:
  17. for pid in public_ids:
  18. db.add(
  19. Announcement(
  20. public_id=pid,
  21. level=level,
  22. texts=json.dumps({"en": {"title": f"T {pid}", "body": "B"}, "de": {"title": "D", "body": "B"}}),
  23. )
  24. )
  25. await db.commit()
  26. async def _setting(db, key: str, value: str) -> None:
  27. db.add(Settings(key=key, value=value))
  28. await db.commit()
  29. async def _get(client: AsyncClient, headers: dict | None = None) -> dict:
  30. r = await client.get(URL, headers=headers)
  31. assert r.status_code == 200, r.text
  32. return r.json()
  33. async def _items(client: AsyncClient, headers: dict | None = None) -> list[dict]:
  34. return (await _get(client, headers))["announcements"]
  35. async def _admin(client: AsyncClient, username: str) -> dict:
  36. await client.post(
  37. "/api/v1/auth/setup",
  38. json={"auth_enabled": True, "admin_username": username, "admin_password": _FIXTURE_PW},
  39. )
  40. login = await client.post("/api/v1/auth/login", json={"username": username, "password": _FIXTURE_PW})
  41. assert login.status_code == 200, login.text
  42. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  43. async def _user(client: AsyncClient, admin: dict, username: str) -> dict:
  44. grp = await client.post(
  45. "/api/v1/groups/", headers=admin, json={"name": f"ann_{username}", "permissions": ["printers:read"]}
  46. )
  47. assert grp.status_code == 201, grp.text
  48. created = await client.post(
  49. "/api/v1/users/",
  50. headers=admin,
  51. json={"username": username, "password": _FIXTURE_PW, "role": "user", "group_ids": [grp.json()["id"]]},
  52. )
  53. assert created.status_code == 201, created.text
  54. login = await client.post("/api/v1/auth/login", json={"username": username, "password": _FIXTURE_PW})
  55. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  56. @pytest.mark.integration
  57. class TestAuthOff:
  58. @pytest.mark.asyncio
  59. async def test_listed_with_every_language_and_unread(self, async_client: AsyncClient, db_session):
  60. await _seed(db_session, "a1")
  61. body = await _get(async_client)
  62. assert body["visible"] is True
  63. [item] = body["announcements"]
  64. assert item["id"] == "a1" and item["level"] == "important" and item["read"] is False
  65. assert set(item["texts"]) == {"en", "de"}
  66. @pytest.mark.asyncio
  67. async def test_read_sticks(self, async_client: AsyncClient, db_session):
  68. await _seed(db_session, "a1")
  69. assert (await async_client.post(f"{URL}/a1/read")).status_code == 204
  70. assert (await _items(async_client))[0]["read"] is True
  71. @pytest.mark.asyncio
  72. async def test_visible_with_nothing_published(self, async_client: AsyncClient):
  73. # The sidebar entry stays for whoever may see announcements, so an empty
  74. # inbox has to say "visible", not look like "not for you".
  75. assert await _get(async_client) == {"visible": True, "announcements": []}
  76. @pytest.mark.asyncio
  77. async def test_unknown_id_is_404(self, async_client: AsyncClient):
  78. assert (await async_client.post(f"{URL}/nope/read")).status_code == 404
  79. @pytest.mark.asyncio
  80. async def test_switched_off_shows_nothing(self, async_client: AsyncClient, db_session):
  81. await _seed(db_session, "a1")
  82. await _setting(db_session, "announcements_enabled", "false")
  83. assert await _get(async_client) == {"visible": False, "announcements": []}
  84. assert (await async_client.post(f"{URL}/a1/read")).status_code == 404
  85. @pytest.mark.integration
  86. class TestAuthOn:
  87. @pytest.mark.asyncio
  88. async def test_signed_out_is_401(self, async_client: AsyncClient, db_session):
  89. await _admin(async_client, "annadmin0")
  90. assert (await async_client.get(URL)).status_code == 401
  91. @pytest.mark.asyncio
  92. async def test_admin_sees_them_a_user_does_not(self, async_client: AsyncClient, db_session):
  93. await _seed(db_session, "a1")
  94. admin = await _admin(async_client, "annadmin1")
  95. user = await _user(async_client, admin, "annuser1")
  96. admin_view = await _get(async_client, admin)
  97. assert admin_view["visible"] is True
  98. assert [i["id"] for i in admin_view["announcements"]] == ["a1"]
  99. assert await _get(async_client, user) == {"visible": False, "announcements": []}
  100. assert (await async_client.post(f"{URL}/a1/read", headers=user)).status_code == 404
  101. @pytest.mark.asyncio
  102. async def test_all_users_setting_shows_them_with_separate_read_state(self, async_client: AsyncClient, db_session):
  103. await _seed(db_session, "a1")
  104. admin = await _admin(async_client, "annadmin2")
  105. user = await _user(async_client, admin, "annuser2")
  106. await _setting(db_session, "announcements_all_users", "true")
  107. assert (await async_client.post(f"{URL}/a1/read", headers=user)).status_code == 204
  108. user_view = await _get(async_client, user)
  109. assert user_view["visible"] is True
  110. assert user_view["announcements"][0]["read"] is True
  111. assert (await _items(async_client, admin))[0]["read"] is False
  112. @pytest.mark.asyncio
  113. async def test_admin_entry_visible_with_nothing_published(self, async_client: AsyncClient):
  114. admin = await _admin(async_client, "annadmin3")
  115. user = await _user(async_client, admin, "annuser3")
  116. assert await _get(async_client, admin) == {"visible": True, "announcements": []}
  117. assert (await _get(async_client, user))["visible"] is False
  118. @pytest.mark.integration
  119. class TestSettings:
  120. @pytest.mark.asyncio
  121. async def test_both_switches_round_trip(self, async_client: AsyncClient):
  122. defaults = (await async_client.get("/api/v1/settings/")).json()
  123. assert defaults["announcements_enabled"] is True
  124. assert defaults["announcements_all_users"] is False
  125. r = await async_client.put(
  126. "/api/v1/settings/", json={"announcements_enabled": False, "announcements_all_users": True}
  127. )
  128. assert r.status_code == 200, r.text
  129. after = (await async_client.get("/api/v1/settings/")).json()
  130. assert after["announcements_enabled"] is False
  131. assert after["announcements_all_users"] is True