test_smart_plug_rest_fields.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. """A REST plug's URLs and request bodies reach only those who may edit plugs.
  2. They commonly hold the device's credential: a login or a token in the URL, a
  3. webhook id in its path, or a token in the body. Readers switch and poll a plug
  4. by its id and never need them, so they get none; editors keep them to edit.
  5. """
  6. from __future__ import annotations
  7. import pytest
  8. from httpx import AsyncClient
  9. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  10. REST = {
  11. "rest_on_url": "http://admin:on-value@plug.elsewhere.example/relay/0?turn=on",
  12. "rest_off_url": "http://plug.elsewhere.example/relay/0?turn=off&auth_key=off-value",
  13. "rest_status_url": "http://plug.elsewhere.example/api/webhook/status-value",
  14. "rest_power_url": "http://admin:power-value@plug.elsewhere.example/status",
  15. "rest_energy_url": "http://admin:energy-value@plug.elsewhere.example/energy",
  16. "rest_on_body": '{"token": "body-on-value"}',
  17. "rest_off_body": '{"token": "body-off-value"}',
  18. }
  19. HIDDEN = tuple(REST)
  20. async def _admin(client: AsyncClient) -> dict[str, str]:
  21. await client.post(
  22. "/api/v1/auth/setup",
  23. json={"auth_enabled": True, "admin_username": "plugadmin", "admin_password": "AdminPass1!"},
  24. )
  25. login = await client.post("/api/v1/auth/login", json={"username": "plugadmin", "password": "AdminPass1!"})
  26. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  27. async def _viewer(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
  28. group = await client.post(
  29. "/api/v1/groups/",
  30. headers=admin,
  31. json={"name": "plugview", "permissions": ["smart_plugs:read", "printers:read"]},
  32. )
  33. await client.post(
  34. "/api/v1/users/",
  35. headers=admin,
  36. json={"username": "plugviewer", "password": "UserPass1!", "group_ids": [group.json()["id"]]},
  37. )
  38. login = await client.post("/api/v1/auth/login", json={"username": "plugviewer", "password": "UserPass1!"})
  39. return {"Authorization": f"Bearer {login.json()['access_token']}"}
  40. async def _api_key(client: AsyncClient, admin: dict[str, str]) -> dict[str, str]:
  41. key = await client.post("/api/v1/api-keys/", headers=admin, json={"name": "reader", "can_read_status": True})
  42. return {"X-API-Key": key.json()["key"]}
  43. @pytest.fixture
  44. async def rest_plug(printer_factory, smart_plug_factory):
  45. printer = await printer_factory(name="P")
  46. return await smart_plug_factory(
  47. name="Rest", plug_type="rest", ip_address=None, printer_id=printer.id, rest_method="GET", **REST
  48. )
  49. async def _bodies(client: AsyncClient, headers: dict[str, str], plug) -> list[dict]:
  50. listed = await client.get("/api/v1/smart-plugs/", headers=headers)
  51. single = await client.get(f"/api/v1/smart-plugs/{plug.id}", headers=headers)
  52. by_printer = await client.get(f"/api/v1/smart-plugs/by-printer/{plug.printer_id}", headers=headers)
  53. for response in (listed, single, by_printer):
  54. assert response.status_code == 200, response.text
  55. return [listed.json()[0], single.json(), by_printer.json()]
  56. @pytest.mark.parametrize("who", ["viewer", "api_key"])
  57. async def test_a_reader_gets_no_rest_urls_or_bodies(async_client: AsyncClient, rest_plug, who):
  58. admin = await _admin(async_client)
  59. headers = await (_viewer if who == "viewer" else _api_key)(async_client, admin)
  60. bodies = await _bodies(async_client, headers, rest_plug)
  61. for body in bodies:
  62. assert {field: body[field] for field in HIDDEN} == dict.fromkeys(HIDDEN)
  63. assert body["plug_type"] == "rest" and body["name"] == "Rest"
  64. assert "-value" not in str(bodies)
  65. async def test_an_editor_gets_the_rest_urls_and_bodies(async_client: AsyncClient, rest_plug):
  66. admin = await _admin(async_client)
  67. for body in await _bodies(async_client, admin, rest_plug):
  68. assert {field: body[field] for field in HIDDEN} == REST
  69. async def test_an_update_answers_with_and_keeps_the_rest_fields(async_client: AsyncClient, rest_plug, db_session):
  70. from backend.app.models.smart_plug import SmartPlug
  71. plug_id = rest_plug.id
  72. admin = await _admin(async_client)
  73. response = await async_client.patch(f"/api/v1/smart-plugs/{plug_id}", headers=admin, json={"name": "Renamed"})
  74. assert response.status_code == 200, response.text
  75. assert {field: response.json()[field] for field in HIDDEN} == REST
  76. db_session.expire_all()
  77. stored = await db_session.get(SmartPlug, plug_id)
  78. assert {field: getattr(stored, field) for field in HIDDEN} == REST
  79. async def test_with_authentication_off_the_rest_fields_are_shown(async_client: AsyncClient, rest_plug):
  80. response = await async_client.get(f"/api/v1/smart-plugs/{rest_plug.id}")
  81. assert response.status_code == 200, response.text
  82. assert {field: response.json()[field] for field in HIDDEN} == REST