test_system_support_vp_scope.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """System info, support logs and virtual printers keep to the caller's printers."""
  2. from __future__ import annotations
  3. from types import SimpleNamespace
  4. from unittest.mock import AsyncMock, patch
  5. import pytest
  6. from httpx import AsyncClient
  7. from backend.app.services.log_health import LogFinding, ScanResult
  8. from backend.app.services.printer_manager import printer_manager
  9. from backend.app.services.virtual_printer import virtual_printer_manager
  10. from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
  11. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  12. PERMS = ["printers:read", "system:read", "settings:read", "settings:update"]
  13. @pytest.fixture(autouse=True)
  14. def _no_vp_services():
  15. with (
  16. patch.object(virtual_printer_manager, "sync_from_db", AsyncMock()),
  17. patch.object(virtual_printer_manager, "configure", AsyncMock()),
  18. ):
  19. yield
  20. async def _setup(client: AsyncClient, printer_factory, permissions=PERMS):
  21. a = await printer_factory(name="Ours", serial_number="00M09A000000111")
  22. b = await printer_factory(name="Theirs", serial_number="00M09A000000222")
  23. token = await _admin_token(client)
  24. perms = await _group(client, token, "perms", permissions=permissions)
  25. team = await _group(client, token, "team", printer_ids=[a.id])
  26. member = _auth((await _user(client, token, "member", [perms, team]))[0])
  27. return a.id, b.id, _auth(token), member
  28. async def test_system_info_lists_and_counts_only_the_caller_printers(async_client: AsyncClient, printer_factory):
  29. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  30. connected = SimpleNamespace(state=SimpleNamespace(connected=True, state="IDLE"))
  31. with patch.dict(printer_manager._clients, {a_id: connected, b_id: connected}):
  32. mine = (await async_client.get("/api/v1/system/info", headers=member)).json()["printers"]
  33. everyone = (await async_client.get("/api/v1/system/info", headers=admin)).json()["printers"]
  34. assert [p["name"] for p in mine["connected_list"]] == ["Ours"]
  35. assert (mine["total"], mine["connected"]) == (1, 1)
  36. assert (everyone["total"], everyone["connected"]) == (2, 2)
  37. async def test_health_findings_come_without_log_lines_for_a_limited_caller(async_client: AsyncClient, printer_factory):
  38. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  39. def scan(**_):
  40. finding = LogFinding(
  41. signature_id="x",
  42. severity="error",
  43. category="bug",
  44. wiki_anchor="x",
  45. count=1,
  46. first_seen="t",
  47. last_seen="t",
  48. sample="a line",
  49. )
  50. return ScanResult(findings=[finding], scanned_entries=1, log_available=True, summary={"total": 1})
  51. with patch("backend.app.api.routes.system.scan_logs", scan):
  52. mine = (await async_client.get("/api/v1/system/health", headers=member)).json()
  53. everyone = (await async_client.get("/api/v1/system/health", headers=admin)).json()
  54. assert [(f["signature_id"], f["sample"]) for f in mine["findings"]] == [("x", "")]
  55. assert everyone["findings"][0]["sample"] == "a line"
  56. async def test_logs_and_the_support_bundle_need_settings_update(async_client: AsyncClient, printer_factory):
  57. a_id, b_id, admin, member = await _setup(
  58. async_client, printer_factory, permissions=["printers:read", "settings:read"]
  59. )
  60. logs = await async_client.get("/api/v1/support/logs", headers=member)
  61. bundle = await async_client.get("/api/v1/support/bundle", headers=member)
  62. assert (logs.status_code, bundle.status_code) == (403, 403)
  63. async def test_a_virtual_printer_bridging_to_another_printer_is_not_shown(async_client: AsyncClient, printer_factory):
  64. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  65. created = await async_client.post(
  66. "/api/v1/virtual-printers", headers=admin, json={"name": "proxy", "mode": "proxy", "target_printer_id": b_id}
  67. )
  68. vp_id = created.json()["id"]
  69. plain = (await async_client.post("/api/v1/virtual-printers", headers=admin, json={"name": "plain"})).json()["id"]
  70. listed = (await async_client.get("/api/v1/virtual-printers", headers=member)).json()
  71. got = await async_client.get(f"/api/v1/virtual-printers/{vp_id}", headers=member)
  72. diagnosed = await async_client.get(f"/api/v1/virtual-printers/{vp_id}/diagnostic", headers=member)
  73. missing = await async_client.get("/api/v1/virtual-printers/999999", headers=member)
  74. entries = listed["printers"] if isinstance(listed, dict) else listed
  75. assert [vp["id"] for vp in entries] == [plain]
  76. assert (
  77. (got.status_code, got.json())
  78. == (diagnosed.status_code, diagnosed.json())
  79. == (
  80. missing.status_code,
  81. missing.json(),
  82. )
  83. )
  84. assert missing.status_code == 404
  85. async def test_a_member_cannot_point_a_virtual_printer_at_another_printer(async_client: AsyncClient, printer_factory):
  86. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  87. theirs = (
  88. await async_client.post(
  89. "/api/v1/virtual-printers", headers=admin, json={"name": "t", "mode": "proxy", "target_printer_id": b_id}
  90. )
  91. ).json()["id"]
  92. ours = (
  93. await async_client.post(
  94. "/api/v1/virtual-printers", headers=member, json={"name": "o", "mode": "proxy", "target_printer_id": a_id}
  95. )
  96. ).json()["id"]
  97. created = await async_client.post(
  98. "/api/v1/virtual-printers", headers=member, json={"name": "x", "mode": "proxy", "target_printer_id": b_id}
  99. )
  100. missing = await async_client.post(
  101. "/api/v1/virtual-printers", headers=member, json={"name": "x", "mode": "proxy", "target_printer_id": 999_999}
  102. )
  103. retargeted = await async_client.put(
  104. f"/api/v1/virtual-printers/{ours}", headers=member, json={"target_printer_id": b_id}
  105. )
  106. changed = await async_client.put(f"/api/v1/virtual-printers/{theirs}", headers=member, json={"name": "mine"})
  107. deleted = await async_client.delete(f"/api/v1/virtual-printers/{theirs}", headers=member)
  108. renamed = await async_client.put(f"/api/v1/virtual-printers/{ours}", headers=member, json={"name": "ours2"})
  109. await async_client.put(
  110. f"/api/v1/virtual-printers/{theirs}", headers=admin, json={"bind_ip": "192.0.2.10", "enabled": True}
  111. )
  112. clashing = await async_client.put(
  113. f"/api/v1/virtual-printers/{ours}", headers=member, json={"bind_ip": "192.0.2.10", "enabled": True}
  114. )
  115. assert clashing.status_code == 400
  116. assert "'" not in clashing.json()["detail"]
  117. assert created.status_code == missing.status_code == 400
  118. assert created.json()["detail"] == f"Printer with ID {b_id} not found"
  119. assert retargeted.status_code == 400
  120. assert (changed.status_code, deleted.status_code) == (404, 404)
  121. assert renamed.status_code == 200
  122. kept = (await async_client.get(f"/api/v1/virtual-printers/{theirs}", headers=admin)).json()
  123. assert (kept["name"], kept["target_printer_id"]) == ("t", b_id)
  124. assert (await async_client.get(f"/api/v1/virtual-printers/{ours}", headers=admin)).json()[
  125. "target_printer_id"
  126. ] == a_id
  127. async def test_the_legacy_virtual_printer_settings_keep_to_the_caller_printers(
  128. async_client: AsyncClient, printer_factory, db_session
  129. ):
  130. from backend.app.models.settings import Settings
  131. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  132. # Stored by an older version, which still let this route aim it.
  133. db_session.add(Settings(key="virtual_printer_target_printer_id", value=str(b_id)))
  134. await db_session.commit()
  135. shown = await async_client.get("/api/v1/settings/virtual-printer", headers=member)
  136. changed = await async_client.put("/api/v1/settings/virtual-printer?archive_name_source=filename", headers=member)
  137. assert shown.json()["target_printer_id"] is None
  138. assert changed.status_code == 404
  139. # The settings also reach the first virtual printer: with the stored target
  140. # now one the member may use, the 404 comes from that row alone.
  141. from sqlalchemy import update
  142. await db_session.execute(
  143. update(Settings).where(Settings.key == "virtual_printer_target_printer_id").values(value=str(a_id))
  144. )
  145. await db_session.commit()
  146. allowed = await async_client.put("/api/v1/settings/virtual-printer?archive_name_source=filename", headers=member)
  147. assert allowed.status_code == 200, allowed.text
  148. await async_client.post(
  149. "/api/v1/virtual-printers", headers=admin, json={"name": "first", "mode": "proxy", "target_printer_id": b_id}
  150. )
  151. toggled = await async_client.put("/api/v1/settings/virtual-printer?tailscale_disabled=false", headers=member)
  152. assert toggled.status_code == 404
  153. async def test_the_legacy_status_leaves_out_what_names_the_target(async_client: AsyncClient, printer_factory):
  154. a_id, b_id, admin, member = await _setup(async_client, printer_factory)
  155. status = {
  156. "running": True,
  157. "name": "N",
  158. "serial": "S",
  159. "target_printer_ip": "192.0.2.1",
  160. "proxy": {"target_host": "192.0.2.1"},
  161. }
  162. with patch.object(virtual_printer_manager, "get_status", return_value=status):
  163. mine = (await async_client.get("/api/v1/settings/virtual-printer", headers=member)).json()["status"]
  164. everyone = (await async_client.get("/api/v1/settings/virtual-printer", headers=admin)).json()["status"]
  165. assert mine == {"running": True}
  166. assert everyone == status