test_settings_api.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. """Integration tests for Settings API endpoints.
  2. Tests the full request/response cycle for /api/v1/settings/ endpoints.
  3. """
  4. import pytest
  5. from httpx import AsyncClient
  6. class TestSettingsAPI:
  7. """Integration tests for /api/v1/settings/ endpoints."""
  8. # ========================================================================
  9. # Get settings
  10. # ========================================================================
  11. @pytest.mark.asyncio
  12. @pytest.mark.integration
  13. async def test_get_settings(self, async_client: AsyncClient):
  14. """Verify settings can be retrieved."""
  15. response = await async_client.get("/api/v1/settings/")
  16. assert response.status_code == 200
  17. result = response.json()
  18. # Check for actual settings fields
  19. assert "auto_archive" in result
  20. assert "currency" in result
  21. assert "date_format" in result
  22. @pytest.mark.asyncio
  23. @pytest.mark.integration
  24. async def test_get_settings_has_defaults(self, async_client: AsyncClient):
  25. """Verify default settings values are returned."""
  26. response = await async_client.get("/api/v1/settings/")
  27. assert response.status_code == 200
  28. result = response.json()
  29. # Verify some default values
  30. assert isinstance(result["auto_archive"], bool)
  31. assert isinstance(result["currency"], str)
  32. # ========================================================================
  33. # Update settings
  34. # ========================================================================
  35. @pytest.mark.asyncio
  36. @pytest.mark.integration
  37. async def test_update_auto_archive(self, async_client: AsyncClient):
  38. """Verify auto_archive can be updated."""
  39. # First get current value
  40. response = await async_client.get("/api/v1/settings/")
  41. original = response.json()["auto_archive"]
  42. # Update to opposite value
  43. new_value = not original
  44. response = await async_client.put("/api/v1/settings/", json={"auto_archive": new_value})
  45. assert response.status_code == 200
  46. assert response.json()["auto_archive"] == new_value
  47. @pytest.mark.asyncio
  48. @pytest.mark.integration
  49. async def test_update_currency(self, async_client: AsyncClient):
  50. """Verify currency can be updated."""
  51. response = await async_client.put("/api/v1/settings/", json={"currency": "EUR"})
  52. assert response.status_code == 200
  53. assert response.json()["currency"] == "EUR"
  54. @pytest.mark.asyncio
  55. @pytest.mark.integration
  56. async def test_update_date_format(self, async_client: AsyncClient):
  57. """Verify date format can be updated."""
  58. response = await async_client.put("/api/v1/settings/", json={"date_format": "eu"})
  59. assert response.status_code == 200
  60. assert response.json()["date_format"] == "eu"
  61. @pytest.mark.asyncio
  62. @pytest.mark.integration
  63. async def test_update_time_format(self, async_client: AsyncClient):
  64. """Verify time format can be updated."""
  65. response = await async_client.put("/api/v1/settings/", json={"time_format": "24h"})
  66. assert response.status_code == 200
  67. assert response.json()["time_format"] == "24h"
  68. @pytest.mark.asyncio
  69. @pytest.mark.integration
  70. async def test_update_filament_cost(self, async_client: AsyncClient):
  71. """Verify default filament cost can be updated."""
  72. response = await async_client.put("/api/v1/settings/", json={"default_filament_cost": 30.0})
  73. assert response.status_code == 200
  74. assert response.json()["default_filament_cost"] == 30.0
  75. @pytest.mark.asyncio
  76. @pytest.mark.integration
  77. async def test_update_energy_cost(self, async_client: AsyncClient):
  78. """Verify energy cost can be updated."""
  79. response = await async_client.put("/api/v1/settings/", json={"energy_cost_per_kwh": 0.20})
  80. assert response.status_code == 200
  81. assert response.json()["energy_cost_per_kwh"] == 0.20
  82. @pytest.mark.asyncio
  83. @pytest.mark.integration
  84. async def test_update_multiple_settings(self, async_client: AsyncClient):
  85. """Verify multiple settings can be updated at once."""
  86. response = await async_client.put(
  87. "/api/v1/settings/",
  88. json={
  89. "currency": "GBP",
  90. "date_format": "iso",
  91. "time_format": "12h",
  92. "save_thumbnails": False,
  93. },
  94. )
  95. assert response.status_code == 200
  96. result = response.json()
  97. assert result["currency"] == "GBP"
  98. assert result["date_format"] == "iso"
  99. assert result["time_format"] == "12h"
  100. assert result["save_thumbnails"] is False
  101. @pytest.mark.asyncio
  102. @pytest.mark.integration
  103. async def test_update_spoolman_settings(self, async_client: AsyncClient):
  104. """Verify Spoolman settings can be updated."""
  105. response = await async_client.put(
  106. "/api/v1/settings/",
  107. json={
  108. "spoolman_enabled": True,
  109. "spoolman_url": "http://localhost:7912",
  110. "spoolman_sync_mode": "manual",
  111. },
  112. )
  113. assert response.status_code == 200
  114. result = response.json()
  115. assert result["spoolman_enabled"] is True
  116. assert result["spoolman_url"] == "http://localhost:7912"
  117. assert result["spoolman_sync_mode"] == "manual"
  118. @pytest.mark.asyncio
  119. @pytest.mark.integration
  120. async def test_update_ams_thresholds(self, async_client: AsyncClient):
  121. """Verify AMS threshold settings can be updated."""
  122. response = await async_client.put(
  123. "/api/v1/settings/",
  124. json={
  125. "ams_humidity_good": 35,
  126. "ams_humidity_fair": 55,
  127. "ams_temp_good": 25.0,
  128. "ams_temp_fair": 32.0,
  129. },
  130. )
  131. assert response.status_code == 200
  132. result = response.json()
  133. assert result["ams_humidity_good"] == 35
  134. assert result["ams_humidity_fair"] == 55
  135. assert result["ams_temp_good"] == 25.0
  136. assert result["ams_temp_fair"] == 32.0
  137. @pytest.mark.asyncio
  138. @pytest.mark.integration
  139. async def test_update_notification_language(self, async_client: AsyncClient):
  140. """Verify notification language can be updated."""
  141. response = await async_client.put("/api/v1/settings/", json={"notification_language": "de"})
  142. assert response.status_code == 200
  143. assert response.json()["notification_language"] == "de"
  144. # ========================================================================
  145. # Settings persistence tests
  146. # ========================================================================
  147. @pytest.mark.asyncio
  148. @pytest.mark.integration
  149. async def test_update_theme_settings(self, async_client: AsyncClient):
  150. """Verify theme settings can be updated."""
  151. response = await async_client.put(
  152. "/api/v1/settings/",
  153. json={
  154. "dark_style": "glow",
  155. "dark_background": "forest",
  156. "dark_accent": "teal",
  157. "light_style": "vibrant",
  158. "light_background": "warm",
  159. "light_accent": "blue",
  160. },
  161. )
  162. assert response.status_code == 200
  163. result = response.json()
  164. assert result["dark_style"] == "glow"
  165. assert result["dark_background"] == "forest"
  166. assert result["dark_accent"] == "teal"
  167. assert result["light_style"] == "vibrant"
  168. assert result["light_background"] == "warm"
  169. assert result["light_accent"] == "blue"
  170. @pytest.mark.asyncio
  171. @pytest.mark.integration
  172. async def test_settings_persist_after_update(self, async_client: AsyncClient):
  173. """CRITICAL: Verify settings changes persist across requests."""
  174. # Update settings
  175. await async_client.put("/api/v1/settings/", json={"currency": "JPY", "check_updates": False})
  176. # Verify persistence in new request
  177. response = await async_client.get("/api/v1/settings/")
  178. result = response.json()
  179. assert result["currency"] == "JPY"
  180. assert result["check_updates"] is False
  181. @pytest.mark.asyncio
  182. @pytest.mark.integration
  183. async def test_update_check_printer_firmware(self, async_client: AsyncClient):
  184. """Verify check_printer_firmware can be updated."""
  185. # Default should be True
  186. response = await async_client.get("/api/v1/settings/")
  187. assert response.json()["check_printer_firmware"] is True
  188. # Update to False
  189. response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": False})
  190. assert response.status_code == 200
  191. assert response.json()["check_printer_firmware"] is False
  192. # Verify persistence
  193. response = await async_client.get("/api/v1/settings/")
  194. assert response.json()["check_printer_firmware"] is False
  195. # Update back to True
  196. response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": True})
  197. assert response.status_code == 200
  198. assert response.json()["check_printer_firmware"] is True
  199. # ========================================================================
  200. # MQTT settings tests
  201. # ========================================================================
  202. @pytest.mark.asyncio
  203. @pytest.mark.integration
  204. async def test_update_mqtt_settings(self, async_client: AsyncClient):
  205. """Verify MQTT settings can be updated."""
  206. response = await async_client.put(
  207. "/api/v1/settings/",
  208. json={
  209. "mqtt_enabled": True,
  210. "mqtt_broker": "mqtt.example.com",
  211. "mqtt_port": 8883,
  212. "mqtt_username": "testuser",
  213. "mqtt_password": "testpass",
  214. "mqtt_topic_prefix": "myprefix",
  215. "mqtt_use_tls": True,
  216. },
  217. )
  218. assert response.status_code == 200
  219. result = response.json()
  220. assert result["mqtt_enabled"] is True
  221. assert result["mqtt_broker"] == "mqtt.example.com"
  222. assert result["mqtt_port"] == 8883
  223. assert result["mqtt_username"] == "testuser"
  224. assert result["mqtt_password"] == "testpass"
  225. assert result["mqtt_topic_prefix"] == "myprefix"
  226. assert result["mqtt_use_tls"] is True
  227. @pytest.mark.asyncio
  228. @pytest.mark.integration
  229. async def test_mqtt_status_endpoint(self, async_client: AsyncClient):
  230. """Verify MQTT status endpoint returns expected fields."""
  231. response = await async_client.get("/api/v1/settings/mqtt/status")
  232. assert response.status_code == 200
  233. result = response.json()
  234. assert "enabled" in result
  235. assert "connected" in result
  236. assert "broker" in result
  237. assert "port" in result
  238. assert "topic_prefix" in result
  239. @pytest.mark.asyncio
  240. @pytest.mark.integration
  241. async def test_mqtt_defaults(self, async_client: AsyncClient):
  242. """Verify MQTT has correct default values."""
  243. # Reset MQTT settings to defaults
  244. await async_client.put(
  245. "/api/v1/settings/",
  246. json={
  247. "mqtt_enabled": False,
  248. "mqtt_broker": "",
  249. "mqtt_port": 1883,
  250. "mqtt_username": "",
  251. "mqtt_password": "",
  252. "mqtt_topic_prefix": "bambuddy",
  253. "mqtt_use_tls": False,
  254. },
  255. )
  256. response = await async_client.get("/api/v1/settings/")
  257. result = response.json()
  258. assert result["mqtt_enabled"] is False
  259. assert result["mqtt_port"] == 1883
  260. assert result["mqtt_topic_prefix"] == "bambuddy"
  261. assert result["mqtt_use_tls"] is False
  262. # ========================================================================
  263. # Camera settings tests
  264. # ========================================================================
  265. @pytest.mark.asyncio
  266. @pytest.mark.integration
  267. async def test_update_camera_view_mode(self, async_client: AsyncClient):
  268. """Verify camera view mode can be updated."""
  269. response = await async_client.put("/api/v1/settings/", json={"camera_view_mode": "embedded"})
  270. assert response.status_code == 200
  271. assert response.json()["camera_view_mode"] == "embedded"
  272. @pytest.mark.asyncio
  273. @pytest.mark.integration
  274. async def test_camera_view_mode_persists(self, async_client: AsyncClient):
  275. """CRITICAL: Verify camera view mode persists after update."""
  276. # Update to embedded
  277. await async_client.put("/api/v1/settings/", json={"camera_view_mode": "embedded"})
  278. # Verify persistence in new request
  279. response = await async_client.get("/api/v1/settings/")
  280. assert response.json()["camera_view_mode"] == "embedded"
  281. # Update back to window
  282. await async_client.put("/api/v1/settings/", json={"camera_view_mode": "window"})
  283. # Verify persistence
  284. response = await async_client.get("/api/v1/settings/")
  285. assert response.json()["camera_view_mode"] == "window"
  286. @pytest.mark.asyncio
  287. @pytest.mark.integration
  288. async def test_camera_view_mode_default(self, async_client: AsyncClient):
  289. """Verify camera view mode has correct default value."""
  290. # Reset by requesting settings (default should be 'window')
  291. response = await async_client.get("/api/v1/settings/")
  292. result = response.json()
  293. assert "camera_view_mode" in result
  294. # Default is 'window' as defined in schema
  295. assert result["camera_view_mode"] in ["window", "embedded"]
  296. # ========================================================================
  297. # Per-printer mapping settings tests
  298. # ========================================================================
  299. @pytest.mark.asyncio
  300. @pytest.mark.integration
  301. async def test_update_per_printer_mapping_expanded(self, async_client: AsyncClient):
  302. """Verify per_printer_mapping_expanded can be updated."""
  303. response = await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": True})
  304. assert response.status_code == 200
  305. assert response.json()["per_printer_mapping_expanded"] is True
  306. @pytest.mark.asyncio
  307. @pytest.mark.integration
  308. async def test_per_printer_mapping_expanded_persists(self, async_client: AsyncClient):
  309. """CRITICAL: Verify per_printer_mapping_expanded persists after update."""
  310. # Update to True
  311. await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": True})
  312. # Verify persistence in new request
  313. response = await async_client.get("/api/v1/settings/")
  314. assert response.json()["per_printer_mapping_expanded"] is True
  315. # Update back to False
  316. await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": False})
  317. # Verify persistence
  318. response = await async_client.get("/api/v1/settings/")
  319. assert response.json()["per_printer_mapping_expanded"] is False
  320. @pytest.mark.asyncio
  321. @pytest.mark.integration
  322. async def test_per_printer_mapping_expanded_default(self, async_client: AsyncClient):
  323. """Verify per_printer_mapping_expanded has correct default value."""
  324. response = await async_client.get("/api/v1/settings/")
  325. result = response.json()
  326. assert "per_printer_mapping_expanded" in result
  327. # Default is False as defined in schema
  328. assert isinstance(result["per_printer_mapping_expanded"], bool)
  329. # ========================================================================
  330. # Backup/Restore tests
  331. # ========================================================================
  332. @pytest.mark.asyncio
  333. @pytest.mark.integration
  334. async def test_backup_includes_external_camera_settings(self, async_client: AsyncClient, printer_factory):
  335. """Verify backup includes external camera settings for printers."""
  336. # Create a printer with external camera settings
  337. _printer = await printer_factory(
  338. name="Camera Test Printer",
  339. external_camera_url="/dev/video0",
  340. external_camera_type="usb",
  341. external_camera_enabled=True,
  342. )
  343. # Request backup with printers
  344. response = await async_client.get("/api/v1/settings/backup?include_printers=true")
  345. assert response.status_code == 200
  346. backup = response.json()
  347. # Find the printer in the backup
  348. assert "printers" in backup
  349. printer_data = next((p for p in backup["printers"] if p["name"] == "Camera Test Printer"), None)
  350. assert printer_data is not None
  351. # Verify external camera fields are included
  352. assert "external_camera_url" in printer_data
  353. assert "external_camera_type" in printer_data
  354. assert "external_camera_enabled" in printer_data
  355. assert printer_data["external_camera_url"] == "/dev/video0"
  356. assert printer_data["external_camera_type"] == "usb"
  357. assert printer_data["external_camera_enabled"] is True
  358. @pytest.mark.asyncio
  359. @pytest.mark.integration
  360. async def test_restore_external_camera_settings_overwrite(self, async_client: AsyncClient, printer_factory):
  361. """Verify restore with overwrite updates external camera settings."""
  362. import io
  363. # Create a printer without camera settings
  364. printer = await printer_factory(
  365. name="Restore Test",
  366. external_camera_url=None,
  367. external_camera_type=None,
  368. external_camera_enabled=False,
  369. )
  370. # Create backup data with camera settings
  371. backup_data = {
  372. "version": "1.0",
  373. "included": ["printers"],
  374. "printers": [
  375. {
  376. "name": "Restore Test",
  377. "serial_number": printer.serial_number,
  378. "ip_address": printer.ip_address,
  379. "external_camera_url": "/dev/video1",
  380. "external_camera_type": "usb",
  381. "external_camera_enabled": True,
  382. }
  383. ],
  384. }
  385. # Restore with overwrite
  386. import json
  387. files = {"file": ("backup.json", io.BytesIO(json.dumps(backup_data).encode()), "application/json")}
  388. response = await async_client.post("/api/v1/settings/restore?overwrite=true", files=files)
  389. assert response.status_code == 200
  390. result = response.json()
  391. assert result["success"] is True
  392. # Verify the printer was updated
  393. response = await async_client.get(f"/api/v1/printers/{printer.id}")
  394. assert response.status_code == 200
  395. updated_printer = response.json()
  396. assert updated_printer["external_camera_url"] == "/dev/video1"
  397. assert updated_printer["external_camera_type"] == "usb"
  398. assert updated_printer["external_camera_enabled"] is True