test_settings_api.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690
  1. """Integration tests for Settings API endpoints.
  2. Tests the full request/response cycle for /api/v1/settings/ endpoints.
  3. """
  4. import os
  5. import pytest
  6. from httpx import AsyncClient
  7. class TestSettingsAPI:
  8. """Integration tests for /api/v1/settings/ endpoints."""
  9. # ========================================================================
  10. # Get settings
  11. # ========================================================================
  12. @pytest.mark.asyncio
  13. @pytest.mark.integration
  14. async def test_get_settings(self, async_client: AsyncClient):
  15. """Verify settings can be retrieved."""
  16. response = await async_client.get("/api/v1/settings/")
  17. assert response.status_code == 200
  18. result = response.json()
  19. # Check for actual settings fields
  20. assert "auto_archive" in result
  21. assert "currency" in result
  22. assert "date_format" in result
  23. @pytest.mark.asyncio
  24. @pytest.mark.integration
  25. async def test_get_settings_has_defaults(self, async_client: AsyncClient):
  26. """Verify default settings values are returned."""
  27. response = await async_client.get("/api/v1/settings/")
  28. assert response.status_code == 200
  29. result = response.json()
  30. # Verify some default values
  31. assert isinstance(result["auto_archive"], bool)
  32. assert isinstance(result["currency"], str)
  33. # ========================================================================
  34. # Update settings
  35. # ========================================================================
  36. @pytest.mark.asyncio
  37. @pytest.mark.integration
  38. async def test_update_auto_archive(self, async_client: AsyncClient):
  39. """Verify auto_archive can be updated."""
  40. # First get current value
  41. response = await async_client.get("/api/v1/settings/")
  42. original = response.json()["auto_archive"]
  43. # Update to opposite value
  44. new_value = not original
  45. response = await async_client.put("/api/v1/settings/", json={"auto_archive": new_value})
  46. assert response.status_code == 200
  47. assert response.json()["auto_archive"] == new_value
  48. @pytest.mark.asyncio
  49. @pytest.mark.integration
  50. async def test_update_currency(self, async_client: AsyncClient):
  51. """Verify currency can be updated."""
  52. response = await async_client.put("/api/v1/settings/", json={"currency": "EUR"})
  53. assert response.status_code == 200
  54. assert response.json()["currency"] == "EUR"
  55. @pytest.mark.asyncio
  56. @pytest.mark.integration
  57. async def test_update_date_format(self, async_client: AsyncClient):
  58. """Verify date format can be updated."""
  59. response = await async_client.put("/api/v1/settings/", json={"date_format": "eu"})
  60. assert response.status_code == 200
  61. assert response.json()["date_format"] == "eu"
  62. @pytest.mark.asyncio
  63. @pytest.mark.integration
  64. async def test_update_time_format(self, async_client: AsyncClient):
  65. """Verify time format can be updated."""
  66. response = await async_client.put("/api/v1/settings/", json={"time_format": "24h"})
  67. assert response.status_code == 200
  68. assert response.json()["time_format"] == "24h"
  69. @pytest.mark.asyncio
  70. @pytest.mark.integration
  71. async def test_update_filament_cost(self, async_client: AsyncClient):
  72. """Verify default filament cost can be updated."""
  73. response = await async_client.put("/api/v1/settings/", json={"default_filament_cost": 30.0})
  74. assert response.status_code == 200
  75. assert response.json()["default_filament_cost"] == 30.0
  76. @pytest.mark.asyncio
  77. @pytest.mark.integration
  78. async def test_update_energy_cost(self, async_client: AsyncClient):
  79. """Verify energy cost can be updated."""
  80. response = await async_client.put("/api/v1/settings/", json={"energy_cost_per_kwh": 0.20})
  81. assert response.status_code == 200
  82. assert response.json()["energy_cost_per_kwh"] == 0.20
  83. @pytest.mark.asyncio
  84. @pytest.mark.integration
  85. async def test_update_multiple_settings(self, async_client: AsyncClient):
  86. """Verify multiple settings can be updated at once."""
  87. response = await async_client.put(
  88. "/api/v1/settings/",
  89. json={
  90. "currency": "GBP",
  91. "date_format": "iso",
  92. "time_format": "12h",
  93. "save_thumbnails": False,
  94. },
  95. )
  96. assert response.status_code == 200
  97. result = response.json()
  98. assert result["currency"] == "GBP"
  99. assert result["date_format"] == "iso"
  100. assert result["time_format"] == "12h"
  101. assert result["save_thumbnails"] is False
  102. @pytest.mark.asyncio
  103. @pytest.mark.integration
  104. async def test_update_spoolman_settings(self, async_client: AsyncClient):
  105. """Verify Spoolman settings can be updated."""
  106. response = await async_client.put(
  107. "/api/v1/settings/",
  108. json={
  109. "spoolman_enabled": True,
  110. "spoolman_url": "http://localhost:7912",
  111. "spoolman_sync_mode": "manual",
  112. },
  113. )
  114. assert response.status_code == 200
  115. result = response.json()
  116. assert result["spoolman_enabled"] is True
  117. assert result["spoolman_url"] == "http://localhost:7912"
  118. assert result["spoolman_sync_mode"] == "manual"
  119. @pytest.mark.asyncio
  120. @pytest.mark.integration
  121. async def test_update_ams_thresholds(self, async_client: AsyncClient):
  122. """Verify AMS threshold settings can be updated."""
  123. response = await async_client.put(
  124. "/api/v1/settings/",
  125. json={
  126. "ams_humidity_good": 35,
  127. "ams_humidity_fair": 55,
  128. "ams_temp_good": 25.0,
  129. "ams_temp_fair": 32.0,
  130. },
  131. )
  132. assert response.status_code == 200
  133. result = response.json()
  134. assert result["ams_humidity_good"] == 35
  135. assert result["ams_humidity_fair"] == 55
  136. assert result["ams_temp_good"] == 25.0
  137. assert result["ams_temp_fair"] == 32.0
  138. @pytest.mark.asyncio
  139. @pytest.mark.integration
  140. async def test_update_notification_language(self, async_client: AsyncClient):
  141. """Verify notification language can be updated."""
  142. response = await async_client.put("/api/v1/settings/", json={"notification_language": "de"})
  143. assert response.status_code == 200
  144. assert response.json()["notification_language"] == "de"
  145. # ========================================================================
  146. # Settings persistence tests
  147. # ========================================================================
  148. @pytest.mark.asyncio
  149. @pytest.mark.integration
  150. async def test_update_theme_settings(self, async_client: AsyncClient):
  151. """Verify theme settings can be updated."""
  152. response = await async_client.put(
  153. "/api/v1/settings/",
  154. json={
  155. "dark_style": "glow",
  156. "dark_background": "forest",
  157. "dark_accent": "teal",
  158. "light_style": "vibrant",
  159. "light_background": "warm",
  160. "light_accent": "blue",
  161. },
  162. )
  163. assert response.status_code == 200
  164. result = response.json()
  165. assert result["dark_style"] == "glow"
  166. assert result["dark_background"] == "forest"
  167. assert result["dark_accent"] == "teal"
  168. assert result["light_style"] == "vibrant"
  169. assert result["light_background"] == "warm"
  170. assert result["light_accent"] == "blue"
  171. @pytest.mark.asyncio
  172. @pytest.mark.integration
  173. async def test_settings_persist_after_update(self, async_client: AsyncClient):
  174. """CRITICAL: Verify settings changes persist across requests."""
  175. # Update settings
  176. await async_client.put("/api/v1/settings/", json={"currency": "JPY", "check_updates": False})
  177. # Verify persistence in new request
  178. response = await async_client.get("/api/v1/settings/")
  179. result = response.json()
  180. assert result["currency"] == "JPY"
  181. assert result["check_updates"] is False
  182. @pytest.mark.asyncio
  183. @pytest.mark.integration
  184. async def test_update_check_printer_firmware(self, async_client: AsyncClient):
  185. """Verify check_printer_firmware can be updated."""
  186. # Default should be True
  187. response = await async_client.get("/api/v1/settings/")
  188. assert response.json()["check_printer_firmware"] is True
  189. # Update to False
  190. response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": False})
  191. assert response.status_code == 200
  192. assert response.json()["check_printer_firmware"] is False
  193. # Verify persistence
  194. response = await async_client.get("/api/v1/settings/")
  195. assert response.json()["check_printer_firmware"] is False
  196. # Update back to True
  197. response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": True})
  198. assert response.status_code == 200
  199. assert response.json()["check_printer_firmware"] is True
  200. # ========================================================================
  201. # MQTT settings tests
  202. # ========================================================================
  203. @pytest.mark.asyncio
  204. @pytest.mark.integration
  205. async def test_update_mqtt_settings(self, async_client: AsyncClient):
  206. """Verify MQTT settings can be updated."""
  207. response = await async_client.put(
  208. "/api/v1/settings/",
  209. json={
  210. "mqtt_enabled": True,
  211. "mqtt_broker": "mqtt.example.com",
  212. "mqtt_port": 8883,
  213. "mqtt_username": "testuser",
  214. "mqtt_password": "testpass",
  215. "mqtt_topic_prefix": "myprefix",
  216. "mqtt_use_tls": True,
  217. },
  218. )
  219. assert response.status_code == 200
  220. result = response.json()
  221. assert result["mqtt_enabled"] is True
  222. assert result["mqtt_broker"] == "mqtt.example.com"
  223. assert result["mqtt_port"] == 8883
  224. assert result["mqtt_username"] == "testuser"
  225. assert result["mqtt_password"] == "testpass"
  226. assert result["mqtt_topic_prefix"] == "myprefix"
  227. assert result["mqtt_use_tls"] is True
  228. @pytest.mark.asyncio
  229. @pytest.mark.integration
  230. async def test_mqtt_status_endpoint(self, async_client: AsyncClient):
  231. """Verify MQTT status endpoint returns expected fields."""
  232. response = await async_client.get("/api/v1/settings/mqtt/status")
  233. assert response.status_code == 200
  234. result = response.json()
  235. assert "enabled" in result
  236. assert "connected" in result
  237. assert "broker" in result
  238. assert "port" in result
  239. assert "topic_prefix" in result
  240. @pytest.mark.asyncio
  241. @pytest.mark.integration
  242. async def test_mqtt_defaults(self, async_client: AsyncClient):
  243. """Verify MQTT has correct default values."""
  244. # Reset MQTT settings to defaults
  245. await async_client.put(
  246. "/api/v1/settings/",
  247. json={
  248. "mqtt_enabled": False,
  249. "mqtt_broker": "",
  250. "mqtt_port": 1883,
  251. "mqtt_username": "",
  252. "mqtt_password": "",
  253. "mqtt_topic_prefix": "bambuddy",
  254. "mqtt_use_tls": False,
  255. },
  256. )
  257. response = await async_client.get("/api/v1/settings/")
  258. result = response.json()
  259. assert result["mqtt_enabled"] is False
  260. assert result["mqtt_port"] == 1883
  261. assert result["mqtt_topic_prefix"] == "bambuddy"
  262. assert result["mqtt_use_tls"] is False
  263. # ========================================================================
  264. # Camera settings tests
  265. # ========================================================================
  266. @pytest.mark.asyncio
  267. @pytest.mark.integration
  268. async def test_update_camera_view_mode(self, async_client: AsyncClient):
  269. """Verify camera view mode can be updated."""
  270. response = await async_client.put("/api/v1/settings/", json={"camera_view_mode": "embedded"})
  271. assert response.status_code == 200
  272. assert response.json()["camera_view_mode"] == "embedded"
  273. @pytest.mark.asyncio
  274. @pytest.mark.integration
  275. async def test_camera_view_mode_persists(self, async_client: AsyncClient):
  276. """CRITICAL: Verify camera view mode persists after update."""
  277. # Update to embedded
  278. await async_client.put("/api/v1/settings/", json={"camera_view_mode": "embedded"})
  279. # Verify persistence in new request
  280. response = await async_client.get("/api/v1/settings/")
  281. assert response.json()["camera_view_mode"] == "embedded"
  282. # Update back to window
  283. await async_client.put("/api/v1/settings/", json={"camera_view_mode": "window"})
  284. # Verify persistence
  285. response = await async_client.get("/api/v1/settings/")
  286. assert response.json()["camera_view_mode"] == "window"
  287. @pytest.mark.asyncio
  288. @pytest.mark.integration
  289. async def test_camera_view_mode_default(self, async_client: AsyncClient):
  290. """Verify camera view mode has correct default value."""
  291. # Reset by requesting settings (default should be 'window')
  292. response = await async_client.get("/api/v1/settings/")
  293. result = response.json()
  294. assert "camera_view_mode" in result
  295. # Default is 'window' as defined in schema
  296. assert result["camera_view_mode"] in ["window", "embedded"]
  297. # ========================================================================
  298. # Per-printer mapping settings tests
  299. # ========================================================================
  300. @pytest.mark.asyncio
  301. @pytest.mark.integration
  302. async def test_update_per_printer_mapping_expanded(self, async_client: AsyncClient):
  303. """Verify per_printer_mapping_expanded can be updated."""
  304. response = await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": True})
  305. assert response.status_code == 200
  306. assert response.json()["per_printer_mapping_expanded"] is True
  307. @pytest.mark.asyncio
  308. @pytest.mark.integration
  309. async def test_per_printer_mapping_expanded_persists(self, async_client: AsyncClient):
  310. """CRITICAL: Verify per_printer_mapping_expanded persists after update."""
  311. # Update to True
  312. await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": True})
  313. # Verify persistence in new request
  314. response = await async_client.get("/api/v1/settings/")
  315. assert response.json()["per_printer_mapping_expanded"] is True
  316. # Update back to False
  317. await async_client.put("/api/v1/settings/", json={"per_printer_mapping_expanded": False})
  318. # Verify persistence
  319. response = await async_client.get("/api/v1/settings/")
  320. assert response.json()["per_printer_mapping_expanded"] is False
  321. @pytest.mark.asyncio
  322. @pytest.mark.integration
  323. async def test_per_printer_mapping_expanded_default(self, async_client: AsyncClient):
  324. """Verify per_printer_mapping_expanded has correct default value."""
  325. response = await async_client.get("/api/v1/settings/")
  326. result = response.json()
  327. assert "per_printer_mapping_expanded" in result
  328. # Default is False as defined in schema
  329. assert isinstance(result["per_printer_mapping_expanded"], bool)
  330. # ========================================================================
  331. # Home Assistant environment variable tests
  332. # ========================================================================
  333. @pytest.mark.asyncio
  334. @pytest.mark.integration
  335. async def test_ha_settings_default_no_env_vars(self, async_client: AsyncClient):
  336. """Verify HA settings work without environment variables (default behavior)."""
  337. # Ensure no env vars are set
  338. os.environ.pop("HA_URL", None)
  339. os.environ.pop("HA_TOKEN", None)
  340. response = await async_client.get("/api/v1/settings/")
  341. result = response.json()
  342. assert response.status_code == 200
  343. assert "ha_enabled" in result
  344. assert "ha_url" in result
  345. assert "ha_token" in result
  346. assert "ha_url_from_env" in result
  347. assert "ha_token_from_env" in result
  348. assert "ha_env_managed" in result
  349. # Default values without env vars
  350. assert result["ha_url_from_env"] is False
  351. assert result["ha_token_from_env"] is False
  352. assert result["ha_env_managed"] is False
  353. @pytest.mark.asyncio
  354. @pytest.mark.integration
  355. async def test_ha_settings_with_both_env_vars(self, async_client: AsyncClient):
  356. """Verify HA settings are overridden when both env vars are set."""
  357. # Set environment variables
  358. os.environ["HA_URL"] = "http://supervisor/core"
  359. os.environ["HA_TOKEN"] = "test-token-12345"
  360. try:
  361. response = await async_client.get("/api/v1/settings/")
  362. result = response.json()
  363. assert response.status_code == 200
  364. # Verify env var values are used
  365. assert result["ha_url"] == "http://supervisor/core"
  366. assert result["ha_token"] == "test-token-12345"
  367. # Verify metadata fields
  368. assert result["ha_url_from_env"] is True
  369. assert result["ha_token_from_env"] is True
  370. assert result["ha_env_managed"] is True
  371. # Verify auto-enable behavior
  372. assert result["ha_enabled"] is True
  373. finally:
  374. # Clean up
  375. os.environ.pop("HA_URL", None)
  376. os.environ.pop("HA_TOKEN", None)
  377. @pytest.mark.asyncio
  378. @pytest.mark.integration
  379. async def test_ha_settings_with_only_url_env_var(self, async_client: AsyncClient):
  380. """Verify partial configuration when only HA_URL is set."""
  381. # Set only URL env var
  382. os.environ["HA_URL"] = "http://supervisor/core"
  383. os.environ.pop("HA_TOKEN", None)
  384. try:
  385. response = await async_client.get("/api/v1/settings/")
  386. result = response.json()
  387. assert response.status_code == 200
  388. # Verify URL is from env, token is from database
  389. assert result["ha_url"] == "http://supervisor/core"
  390. assert result["ha_url_from_env"] is True
  391. assert result["ha_token_from_env"] is False
  392. assert result["ha_env_managed"] is False
  393. # No auto-enable with partial config
  394. assert result["ha_enabled"] is False # Database default
  395. finally:
  396. os.environ.pop("HA_URL", None)
  397. @pytest.mark.asyncio
  398. @pytest.mark.integration
  399. async def test_ha_settings_with_only_token_env_var(self, async_client: AsyncClient):
  400. """Verify partial configuration when only HA_TOKEN is set."""
  401. # Set only token env var
  402. os.environ.pop("HA_URL", None)
  403. os.environ["HA_TOKEN"] = "test-token-12345"
  404. try:
  405. response = await async_client.get("/api/v1/settings/")
  406. result = response.json()
  407. assert response.status_code == 200
  408. # Verify token is from env, URL is from database
  409. assert result["ha_token"] == "test-token-12345"
  410. assert result["ha_url_from_env"] is False
  411. assert result["ha_token_from_env"] is True
  412. assert result["ha_env_managed"] is False
  413. # No auto-enable with partial config
  414. assert result["ha_enabled"] is False # Database default
  415. finally:
  416. os.environ.pop("HA_TOKEN", None)
  417. @pytest.mark.asyncio
  418. @pytest.mark.integration
  419. async def test_ha_settings_env_vars_override_database(self, async_client: AsyncClient):
  420. """Verify environment variables take precedence over database values."""
  421. # First, set database values
  422. await async_client.put(
  423. "/api/v1/settings/",
  424. json={
  425. "ha_enabled": True,
  426. "ha_url": "http://database-url:8123",
  427. "ha_token": "database-token",
  428. },
  429. )
  430. # Verify database values are set
  431. response = await async_client.get("/api/v1/settings/")
  432. result = response.json()
  433. assert result["ha_url"] == "http://database-url:8123"
  434. assert result["ha_token"] == "database-token"
  435. # Now set environment variables
  436. os.environ["HA_URL"] = "http://env-url/core"
  437. os.environ["HA_TOKEN"] = "env-token-xyz"
  438. try:
  439. response = await async_client.get("/api/v1/settings/")
  440. result = response.json()
  441. # Verify env vars override database
  442. assert result["ha_url"] == "http://env-url/core"
  443. assert result["ha_token"] == "env-token-xyz"
  444. assert result["ha_url_from_env"] is True
  445. assert result["ha_token_from_env"] is True
  446. assert result["ha_env_managed"] is True
  447. assert result["ha_enabled"] is True
  448. finally:
  449. os.environ.pop("HA_URL", None)
  450. os.environ.pop("HA_TOKEN", None)
  451. # Verify database values are still there after removing env vars
  452. response = await async_client.get("/api/v1/settings/")
  453. result = response.json()
  454. assert result["ha_url"] == "http://database-url:8123"
  455. assert result["ha_token"] == "database-token"
  456. assert result["ha_url_from_env"] is False
  457. assert result["ha_token_from_env"] is False
  458. @pytest.mark.asyncio
  459. @pytest.mark.integration
  460. async def test_ha_settings_database_updates_accepted_but_ignored(self, async_client: AsyncClient):
  461. """Verify database updates are accepted but have no effect when env vars are set."""
  462. # Set environment variables
  463. os.environ["HA_URL"] = "http://supervisor/core"
  464. os.environ["HA_TOKEN"] = "env-token"
  465. try:
  466. # Attempt to update via API
  467. response = await async_client.put(
  468. "/api/v1/settings/",
  469. json={
  470. "ha_url": "http://different-url:8123",
  471. "ha_token": "different-token",
  472. },
  473. )
  474. # Update should succeed
  475. assert response.status_code == 200
  476. # But values should still be from env vars
  477. result = response.json()
  478. assert result["ha_url"] == "http://supervisor/core"
  479. assert result["ha_token"] == "env-token"
  480. assert result["ha_url_from_env"] is True
  481. assert result["ha_token_from_env"] is True
  482. finally:
  483. os.environ.pop("HA_URL", None)
  484. os.environ.pop("HA_TOKEN", None)
  485. @pytest.mark.asyncio
  486. @pytest.mark.integration
  487. async def test_ha_settings_empty_env_vars_treated_as_not_set(self, async_client: AsyncClient):
  488. """Verify empty environment variables are treated as not set."""
  489. # Set empty env vars
  490. os.environ["HA_URL"] = ""
  491. os.environ["HA_TOKEN"] = ""
  492. try:
  493. response = await async_client.get("/api/v1/settings/")
  494. result = response.json()
  495. # Empty env vars should be treated as not set
  496. assert result["ha_url_from_env"] is False
  497. assert result["ha_token_from_env"] is False
  498. assert result["ha_env_managed"] is False
  499. finally:
  500. os.environ.pop("HA_URL", None)
  501. os.environ.pop("HA_TOKEN", None)
  502. @pytest.mark.asyncio
  503. @pytest.mark.integration
  504. async def test_ha_settings_can_be_updated_normally_without_env_vars(self, async_client: AsyncClient):
  505. """Verify HA settings can be updated normally when env vars are not set."""
  506. # Ensure no env vars
  507. os.environ.pop("HA_URL", None)
  508. os.environ.pop("HA_TOKEN", None)
  509. # Update HA settings
  510. response = await async_client.put(
  511. "/api/v1/settings/",
  512. json={
  513. "ha_enabled": True,
  514. "ha_url": "http://192.168.1.100:8123",
  515. "ha_token": "my-long-lived-token",
  516. },
  517. )
  518. assert response.status_code == 200
  519. result = response.json()
  520. assert result["ha_enabled"] is True
  521. assert result["ha_url"] == "http://192.168.1.100:8123"
  522. assert result["ha_token"] == "my-long-lived-token"
  523. assert result["ha_url_from_env"] is False
  524. assert result["ha_token_from_env"] is False
  525. assert result["ha_env_managed"] is False
  526. # Verify persistence
  527. response = await async_client.get("/api/v1/settings/")
  528. result = response.json()
  529. assert result["ha_enabled"] is True
  530. assert result["ha_url"] == "http://192.168.1.100:8123"
  531. assert result["ha_token"] == "my-long-lived-token"
  532. class TestSimplifiedBackupRestore:
  533. """Integration tests for the simplified backup/restore endpoints (ZIP-based).
  534. Note: Tests that require actual file operations (backup creation) are skipped
  535. because the test suite uses an in-memory database. These tests focus on
  536. validation and error handling which don't require file I/O.
  537. """
  538. @pytest.mark.asyncio
  539. @pytest.mark.integration
  540. async def test_restore_requires_zip_file(self, async_client: AsyncClient):
  541. """Verify restore rejects non-ZIP files."""
  542. files = {"file": ("backup.txt", b"not a zip file", "text/plain")}
  543. response = await async_client.post("/api/v1/settings/restore", files=files)
  544. assert response.status_code == 400
  545. assert "zip" in response.json()["detail"].lower()
  546. @pytest.mark.asyncio
  547. @pytest.mark.integration
  548. async def test_restore_requires_database_in_zip(self, async_client: AsyncClient):
  549. """Verify restore rejects ZIP without database file."""
  550. import io
  551. import zipfile
  552. # Create a ZIP without bambuddy.db
  553. zip_buffer = io.BytesIO()
  554. with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
  555. zf.writestr("dummy.txt", "dummy content")
  556. zip_buffer.seek(0)
  557. files = {"file": ("backup.zip", zip_buffer.read(), "application/zip")}
  558. response = await async_client.post("/api/v1/settings/restore", files=files)
  559. assert response.status_code == 400
  560. assert "missing bambuddy.db" in response.json()["detail"].lower()
  561. @pytest.mark.asyncio
  562. @pytest.mark.integration
  563. async def test_restore_invalid_zip(self, async_client: AsyncClient):
  564. """Verify restore rejects corrupted ZIP files."""
  565. files = {"file": ("backup.zip", b"not valid zip content", "application/zip")}
  566. response = await async_client.post("/api/v1/settings/restore", files=files)
  567. assert response.status_code == 400
  568. assert "not a valid zip" in response.json()["detail"].lower()