test_printer_camera_url_credentials.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. """External camera credentials reach only those who may edit the printer.
  2. The camera login commonly sits in its URL (rtsp://user:password@host/...).
  3. Printer readers got both camera URLs as stored; the printer access code was
  4. already withheld from them. Readers now get the URLs with the password
  5. replaced; editors keep the full URLs they need to edit them.
  6. """
  7. from __future__ import annotations
  8. import pytest
  9. from httpx import AsyncClient
  10. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  11. STREAM = "rtsp://cam:stream-pw@192.168.1.50:554/stream"
  12. SNAPSHOT = "http://cam:snap-pw@192.168.1.50/snapshot.jpg"
  13. async def _admin(client: AsyncClient) -> dict[str, str]:
  14. await client.post(
  15. "/api/v1/auth/setup",
  16. json={"auth_enabled": True, "admin_username": "camadmin", "admin_password": "AdminPass1!"},
  17. )
  18. login = await client.post("/api/v1/auth/login", json={"username": "camadmin", "password": "AdminPass1!"})
  19. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  20. async def _viewer(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
  21. group = await client.post(
  22. "/api/v1/groups/", headers=admin, json={"name": "camview", "permissions": ["printers:read"]}
  23. )
  24. await client.post(
  25. "/api/v1/users/",
  26. headers=admin,
  27. json={"username": "camviewer", "password": "UserPass1!", "group_ids": [group.json()["id"]]},
  28. )
  29. login = await client.post("/api/v1/auth/login", json={"username": "camviewer", "password": "UserPass1!"})
  30. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  31. async def _api_key(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
  32. key = await client.post("/api/v1/api-keys/", headers=admin, json={"name": "reader", "can_read_status": True})
  33. return {"X-API-Key": key.json()["key"]}
  34. @pytest.fixture
  35. async def camera_printer(printer_factory):
  36. return await printer_factory(
  37. name="Cam", external_camera_enabled=True, external_camera_url=STREAM, external_camera_snapshot_url=SNAPSHOT
  38. )
  39. @pytest.mark.parametrize("who", ["viewer", "api_key"])
  40. async def test_a_reader_gets_the_camera_urls_without_their_passwords(async_client: AsyncClient, camera_printer, who):
  41. admin = await _admin(async_client)
  42. headers = await (_viewer if who == "viewer" else _api_key)(async_client, admin)
  43. listed = await async_client.get("/api/v1/printers/", headers=headers)
  44. single = await async_client.get(f"/api/v1/printers/{camera_printer.id}", headers=headers)
  45. assert listed.status_code == 200 and single.status_code == 200, (listed.text, single.text)
  46. for body in (listed.json()[0], single.json()):
  47. assert "stream-pw" not in body["external_camera_url"]
  48. assert "snap-pw" not in body["external_camera_snapshot_url"]
  49. assert body["external_camera_url"].endswith("@192.168.1.50:554/stream")
  50. assert "stream-pw" not in listed.text + single.text
  51. async def test_an_editor_gets_the_full_camera_urls(async_client: AsyncClient, camera_printer):
  52. admin = await _admin(async_client)
  53. single = (await async_client.get(f"/api/v1/printers/{camera_printer.id}", headers=admin)).json()
  54. listed = (await async_client.get("/api/v1/printers/", headers=admin)).json()[0]
  55. for body in (single, listed):
  56. assert body["external_camera_url"] == STREAM
  57. assert body["external_camera_snapshot_url"] == SNAPSHOT
  58. async def test_an_edit_leaves_the_stored_camera_url_alone(async_client: AsyncClient, camera_printer, db_session):
  59. """The update's response is the reader shape; nothing it shows is written back."""
  60. from backend.app.models.printer import Printer
  61. printer_id = camera_printer.id
  62. admin = await _admin(async_client)
  63. response = await async_client.patch(f"/api/v1/printers/{printer_id}", headers=admin, json={"camera_rotation": 90})
  64. assert response.status_code == 200, response.text
  65. db_session.expire_all()
  66. stored = await db_session.get(Printer, printer_id)
  67. assert stored.external_camera_url == STREAM and stored.external_camera_snapshot_url == SNAPSHOT
  68. async def test_the_camera_test_takes_the_url_in_its_body_and_still_in_the_query(
  69. async_client: AsyncClient, camera_printer
  70. ):
  71. """In the body it stays out of the access log; the query form is kept for a
  72. cached older page."""
  73. from unittest.mock import AsyncMock, patch
  74. printer_id = camera_printer.id
  75. admin = await _admin(async_client)
  76. with patch(
  77. "backend.app.services.external_camera.test_connection",
  78. new_callable=AsyncMock,
  79. return_value={"success": True},
  80. ) as test:
  81. body = await async_client.post(
  82. f"/api/v1/printers/{printer_id}/camera/external/test",
  83. headers=admin,
  84. json={"url": STREAM, "camera_type": "rtsp"},
  85. )
  86. query = await async_client.post(
  87. f"/api/v1/printers/{printer_id}/camera/external/test",
  88. headers=admin,
  89. params={"url": STREAM, "camera_type": "rtsp"},
  90. )
  91. assert body.status_code == 200, body.text
  92. assert query.status_code == 200, query.text
  93. assert [c.args for c in test.await_args_list] == [(STREAM, "rtsp"), (STREAM, "rtsp")]