test_smart_plugs_api.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. """Integration tests for Smart Plugs API endpoints.
  2. Tests the full request/response cycle for /api/v1/smart-plugs/ endpoints.
  3. """
  4. import pytest
  5. from httpx import AsyncClient
  6. class TestSmartPlugsAPI:
  7. """Integration tests for /api/v1/smart-plugs/ endpoints."""
  8. # ========================================================================
  9. # List endpoints
  10. # ========================================================================
  11. @pytest.mark.asyncio
  12. @pytest.mark.integration
  13. async def test_list_smart_plugs_empty(self, async_client: AsyncClient):
  14. """Verify empty list is returned when no plugs exist."""
  15. response = await async_client.get("/api/v1/smart-plugs/")
  16. assert response.status_code == 200
  17. assert response.json() == []
  18. @pytest.mark.asyncio
  19. @pytest.mark.integration
  20. async def test_list_smart_plugs_with_data(
  21. self, async_client: AsyncClient, smart_plug_factory, db_session
  22. ):
  23. """Verify list returns existing plugs."""
  24. plug = await smart_plug_factory(name="Test Plug 1")
  25. response = await async_client.get("/api/v1/smart-plugs/")
  26. assert response.status_code == 200
  27. data = response.json()
  28. assert len(data) >= 1
  29. assert any(p["name"] == "Test Plug 1" for p in data)
  30. # ========================================================================
  31. # Create endpoints
  32. # ========================================================================
  33. @pytest.mark.asyncio
  34. @pytest.mark.integration
  35. async def test_create_smart_plug(self, async_client: AsyncClient):
  36. """Verify smart plug can be created."""
  37. data = {
  38. "name": "New Plug",
  39. "ip_address": "192.168.1.100",
  40. "enabled": True,
  41. "auto_on": True,
  42. "auto_off": False,
  43. }
  44. response = await async_client.post("/api/v1/smart-plugs/", json=data)
  45. assert response.status_code == 200
  46. result = response.json()
  47. assert result["name"] == "New Plug"
  48. assert result["ip_address"] == "192.168.1.100"
  49. assert result["auto_off"] is False
  50. @pytest.mark.asyncio
  51. @pytest.mark.integration
  52. async def test_create_smart_plug_with_printer(
  53. self, async_client: AsyncClient, printer_factory, db_session
  54. ):
  55. """Verify smart plug can be linked to a printer."""
  56. printer = await printer_factory(name="Test Printer")
  57. data = {
  58. "name": "Printer Plug",
  59. "ip_address": "192.168.1.101",
  60. "printer_id": printer.id,
  61. }
  62. response = await async_client.post("/api/v1/smart-plugs/", json=data)
  63. assert response.status_code == 200
  64. result = response.json()
  65. assert result["printer_id"] == printer.id
  66. @pytest.mark.asyncio
  67. @pytest.mark.integration
  68. async def test_create_plug_with_invalid_printer_id(
  69. self, async_client: AsyncClient
  70. ):
  71. """Verify creating plug with non-existent printer fails."""
  72. data = {
  73. "name": "Test Plug",
  74. "ip_address": "192.168.1.100",
  75. "printer_id": 9999,
  76. }
  77. response = await async_client.post("/api/v1/smart-plugs/", json=data)
  78. assert response.status_code == 400
  79. assert "Printer not found" in response.json()["detail"]
  80. # ========================================================================
  81. # Get single endpoint
  82. # ========================================================================
  83. @pytest.mark.asyncio
  84. @pytest.mark.integration
  85. async def test_get_smart_plug(
  86. self, async_client: AsyncClient, smart_plug_factory, db_session
  87. ):
  88. """Verify single plug can be retrieved."""
  89. plug = await smart_plug_factory(name="Get Test Plug")
  90. response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
  91. assert response.status_code == 200
  92. result = response.json()
  93. assert result["id"] == plug.id
  94. assert result["name"] == "Get Test Plug"
  95. @pytest.mark.asyncio
  96. @pytest.mark.integration
  97. async def test_get_smart_plug_not_found(self, async_client: AsyncClient):
  98. """Verify 404 for non-existent plug."""
  99. response = await async_client.get("/api/v1/smart-plugs/9999")
  100. assert response.status_code == 404
  101. # ========================================================================
  102. # Update endpoints (CRITICAL - toggle persistence)
  103. # ========================================================================
  104. @pytest.mark.asyncio
  105. @pytest.mark.integration
  106. async def test_update_auto_off_toggle(
  107. self, async_client: AsyncClient, smart_plug_factory, db_session
  108. ):
  109. """CRITICAL: Verify auto_off toggle persists correctly.
  110. This tests the regression scenario where toggling auto_off
  111. wasn't being saved properly.
  112. """
  113. # Create plug with auto_off=True
  114. plug = await smart_plug_factory(auto_off=True)
  115. # Verify initial state
  116. response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
  117. assert response.status_code == 200
  118. assert response.json()["auto_off"] is True
  119. # Toggle auto_off to False
  120. response = await async_client.patch(
  121. f"/api/v1/smart-plugs/{plug.id}",
  122. json={"auto_off": False}
  123. )
  124. assert response.status_code == 200
  125. assert response.json()["auto_off"] is False
  126. # Verify change persisted by fetching again
  127. response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
  128. assert response.json()["auto_off"] is False
  129. @pytest.mark.asyncio
  130. @pytest.mark.integration
  131. async def test_update_auto_on_toggle(
  132. self, async_client: AsyncClient, smart_plug_factory, db_session
  133. ):
  134. """Verify auto_on toggle persists correctly."""
  135. plug = await smart_plug_factory(auto_on=True)
  136. response = await async_client.patch(
  137. f"/api/v1/smart-plugs/{plug.id}",
  138. json={"auto_on": False}
  139. )
  140. assert response.status_code == 200
  141. assert response.json()["auto_on"] is False
  142. # Verify persistence
  143. response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}")
  144. assert response.json()["auto_on"] is False
  145. @pytest.mark.asyncio
  146. @pytest.mark.integration
  147. async def test_update_enabled_toggle(
  148. self, async_client: AsyncClient, smart_plug_factory, db_session
  149. ):
  150. """Verify enabled toggle persists correctly."""
  151. plug = await smart_plug_factory(enabled=True)
  152. response = await async_client.patch(
  153. f"/api/v1/smart-plugs/{plug.id}",
  154. json={"enabled": False}
  155. )
  156. assert response.status_code == 200
  157. assert response.json()["enabled"] is False
  158. @pytest.mark.asyncio
  159. @pytest.mark.integration
  160. async def test_update_off_delay_mode(
  161. self, async_client: AsyncClient, smart_plug_factory, db_session
  162. ):
  163. """Verify off_delay_mode can be changed."""
  164. plug = await smart_plug_factory(off_delay_mode="time")
  165. response = await async_client.patch(
  166. f"/api/v1/smart-plugs/{plug.id}",
  167. json={"off_delay_mode": "temperature", "off_temp_threshold": 50}
  168. )
  169. assert response.status_code == 200
  170. result = response.json()
  171. assert result["off_delay_mode"] == "temperature"
  172. assert result["off_temp_threshold"] == 50
  173. @pytest.mark.asyncio
  174. @pytest.mark.integration
  175. async def test_update_schedule_settings(
  176. self, async_client: AsyncClient, smart_plug_factory, db_session
  177. ):
  178. """Verify schedule settings can be updated."""
  179. plug = await smart_plug_factory(schedule_enabled=False)
  180. response = await async_client.patch(
  181. f"/api/v1/smart-plugs/{plug.id}",
  182. json={
  183. "schedule_enabled": True,
  184. "schedule_on_time": "08:00",
  185. "schedule_off_time": "22:00",
  186. }
  187. )
  188. assert response.status_code == 200
  189. result = response.json()
  190. assert result["schedule_enabled"] is True
  191. assert result["schedule_on_time"] == "08:00"
  192. assert result["schedule_off_time"] == "22:00"
  193. @pytest.mark.asyncio
  194. @pytest.mark.integration
  195. async def test_update_multiple_fields(
  196. self, async_client: AsyncClient, smart_plug_factory, db_session
  197. ):
  198. """Verify multiple fields can be updated at once."""
  199. plug = await smart_plug_factory(
  200. name="Old Name",
  201. auto_on=True,
  202. auto_off=True,
  203. )
  204. response = await async_client.patch(
  205. f"/api/v1/smart-plugs/{plug.id}",
  206. json={
  207. "name": "New Name",
  208. "auto_on": False,
  209. "auto_off": False,
  210. }
  211. )
  212. assert response.status_code == 200
  213. result = response.json()
  214. assert result["name"] == "New Name"
  215. assert result["auto_on"] is False
  216. assert result["auto_off"] is False
  217. # ========================================================================
  218. # Control endpoints
  219. # ========================================================================
  220. @pytest.mark.asyncio
  221. @pytest.mark.integration
  222. async def test_control_smart_plug_on(
  223. self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
  224. ):
  225. """Verify smart plug can be turned on."""
  226. plug = await smart_plug_factory()
  227. response = await async_client.post(
  228. f"/api/v1/smart-plugs/{plug.id}/control",
  229. json={"action": "on"}
  230. )
  231. assert response.status_code == 200
  232. result = response.json()
  233. assert result["success"] is True
  234. assert result["action"] == "on"
  235. @pytest.mark.asyncio
  236. @pytest.mark.integration
  237. async def test_control_smart_plug_off(
  238. self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
  239. ):
  240. """Verify smart plug can be turned off."""
  241. plug = await smart_plug_factory()
  242. response = await async_client.post(
  243. f"/api/v1/smart-plugs/{plug.id}/control",
  244. json={"action": "off"}
  245. )
  246. assert response.status_code == 200
  247. result = response.json()
  248. assert result["success"] is True
  249. assert result["action"] == "off"
  250. @pytest.mark.asyncio
  251. @pytest.mark.integration
  252. async def test_control_smart_plug_toggle(
  253. self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
  254. ):
  255. """Verify smart plug can be toggled."""
  256. plug = await smart_plug_factory()
  257. response = await async_client.post(
  258. f"/api/v1/smart-plugs/{plug.id}/control",
  259. json={"action": "toggle"}
  260. )
  261. assert response.status_code == 200
  262. result = response.json()
  263. assert result["success"] is True
  264. assert result["action"] == "toggle"
  265. @pytest.mark.asyncio
  266. @pytest.mark.integration
  267. async def test_control_invalid_action(
  268. self, async_client: AsyncClient, smart_plug_factory, db_session
  269. ):
  270. """Verify invalid action returns error."""
  271. plug = await smart_plug_factory()
  272. response = await async_client.post(
  273. f"/api/v1/smart-plugs/{plug.id}/control",
  274. json={"action": "invalid"}
  275. )
  276. # FastAPI returns 422 for pydantic validation errors
  277. assert response.status_code == 422
  278. # ========================================================================
  279. # Status endpoint
  280. # ========================================================================
  281. @pytest.mark.asyncio
  282. @pytest.mark.integration
  283. async def test_get_smart_plug_status(
  284. self, async_client: AsyncClient, smart_plug_factory, mock_tasmota_service, db_session
  285. ):
  286. """Verify smart plug status can be retrieved."""
  287. plug = await smart_plug_factory()
  288. response = await async_client.get(f"/api/v1/smart-plugs/{plug.id}/status")
  289. assert response.status_code == 200
  290. result = response.json()
  291. assert result["state"] == "ON"
  292. assert result["reachable"] is True
  293. # ========================================================================
  294. # Delete endpoint
  295. # ========================================================================
  296. @pytest.mark.asyncio
  297. @pytest.mark.integration
  298. async def test_delete_smart_plug(
  299. self, async_client: AsyncClient, smart_plug_factory, db_session
  300. ):
  301. """Verify smart plug can be deleted."""
  302. plug = await smart_plug_factory()
  303. plug_id = plug.id
  304. response = await async_client.delete(f"/api/v1/smart-plugs/{plug_id}")
  305. assert response.status_code == 200
  306. # Verify deleted
  307. response = await async_client.get(f"/api/v1/smart-plugs/{plug_id}")
  308. assert response.status_code == 404
  309. @pytest.mark.asyncio
  310. @pytest.mark.integration
  311. async def test_delete_nonexistent_plug(self, async_client: AsyncClient):
  312. """Verify deleting non-existent plug returns 404."""
  313. response = await async_client.delete("/api/v1/smart-plugs/9999")
  314. assert response.status_code == 404