import aiohttp import pytest from aioresponses import aioresponses import pytest_asyncio from watergate_local_api import WatergateLocalApiClient, WatergateApiException @pytest_asyncio.fixture async def client(): # Create and initialize the client within an async context client = WatergateLocalApiClient(base_url="http://testserver") yield client # Ensure proper cleanup after each test await client.async_close() @pytest.mark.asyncio async def test_get_device_state(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "closed", "waterFlowing": False, "mqttConnected": True, "wifiConnected": True, "powerSupply": "external", "firmwareVersion": "2024.1.0", "waterMeter": {"volume": 567820, "duration": 3908}, "uptime": 1024560, "serialNumber": "123123" }) device_state = await client.async_get_device_state() assert device_state.valve_state == "closed" assert device_state.water_flow_indicator is False assert device_state.mqtt_status is True assert device_state.wifi_status is True assert device_state.power_supply == "external" assert device_state.firmware_version == "2024.1.0" assert device_state.uptime == 1024560 assert device_state.water_meter.volume == 567820 @pytest.mark.asyncio async def test_get_networking(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic/networking", payload={ "mqttConnected": True, "wifiConnected": True, "ip": "192.168.21.37", "gateway": "192.168.1.0", "subnet": "192.168.1.0/24", "ssid": "MyWiFi", "rssi": -45, "wifiUpTime": 1024560, "mqttUpTime": 1023450 }) networking_data = await client.async_get_networking() assert networking_data.ip == "192.168.21.37" assert networking_data.gateway == "192.168.1.0" assert networking_data.subnet == "192.168.1.0/24" assert networking_data.ssid == "MyWiFi" assert networking_data.rssi == -45 @pytest.mark.asyncio async def test_get_telemetry_data(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic/telemetry", payload={ "flow": 6800, "pressure": 2320, "temperature": 23.5, "event": {"volume": 16000, "duration": 90}, "errors": ["flow"] }) telemetry_data = await client.async_get_telemetry_data() assert telemetry_data.flow == 6800 assert telemetry_data.pressure == 2320 assert telemetry_data.water_temperature == 23.5 assert telemetry_data.ongoing_event.volume == 16000 assert telemetry_data.ongoing_event.duration == 90 assert "flow" in telemetry_data.errors @pytest.mark.asyncio async def test_patch_auto_shut_off(client): with aioresponses() as mock: mock.patch("http://testserver/api/sonic/auto-shut-off", status=204) result = await client.async_patch_auto_shut_off(enabled=True, duration=10, volume=5) assert result is True @pytest.mark.asyncio async def test_get_auto_shut_off(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic/auto-shut-off", payload={ "volumeThreshold": 300, "durationThreshold": 60, }) report = await client.async_get_auto_shut_off() assert report.volume_threshold == 300 assert report.duration_threshold == 60 @pytest.mark.asyncio async def test_get_auto_shut_off_report(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic/auto-shut-off/report", payload={ "type": "VOLUME_THRESHOLD", "volume": 300, "duration": 60, "timestamp": 1623456789 }) report = await client.async_get_auto_shut_off_report() assert report.type == "VOLUME_THRESHOLD" assert report.volume == 300 assert report.duration == 60 assert report.timestamp == 1623456789 @pytest.mark.asyncio async def test_set_webhook_url(client): with aioresponses() as mock: mock.put("http://testserver/api/sonic/webhook", status=204) result = await client.async_set_webhook_url("http://webhook.url") assert result is True @pytest.mark.asyncio async def test_set_valve(client): with aioresponses() as mock: mock.put("http://testserver/api/sonic/valve", status=204) result = await client.async_set_valve_state("open") assert result is True @pytest.mark.asyncio async def test_retry_logic(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic", status=500) with pytest.raises(WatergateApiException): await client.async_get_device_state() @pytest.mark.asyncio async def test_custom_exception(client): with aioresponses() as mock: mock.get("http://testserver/api/sonic", exception=aiohttp.ClientError) with pytest.raises(WatergateApiException): await client.async_get_device_state() @pytest.mark.asyncio async def test_api_rate_limit_handling(client): # Simulate API rate limit response (HTTP 429) with aioresponses() as mock: mock.get("http://testserver/api/sonic", status=429) with pytest.raises(WatergateApiException): await client.async_get_device_state() @pytest.mark.asyncio async def test_get_networking_with_unexpected_status_code(client): # Simulate an unexpected status code (HTTP 403) with aioresponses() as mock: mock.get("http://testserver/api/sonic/networking", status=403) with pytest.raises(WatergateApiException): await client.async_get_networking() @pytest.mark.asyncio async def test_set_webhook_url_invalid_response(client): # Simulate an invalid response when setting webhook (e.g., status 400) with aioresponses() as mock: mock.patch("http://testserver/api/sonic/webhook", status=400) with pytest.raises(WatergateApiException): await client.async_set_webhook_url("http://invalid-webhook.url") @pytest.mark.asyncio async def test_auto_shut_off_report_with_missing_fields(client): # Response with some fields missing with aioresponses() as mock: mock.get("http://testserver/api/sonic/auto-shut-off/report", payload={"type": "VOLUME_THRESHOLD"}) report = await client.async_get_auto_shut_off_report() assert report.type == "VOLUME_THRESHOLD" assert report.volume is None # Missing fields should default to None @pytest.mark.asyncio async def test_get_device_state_v2(client): """Test fetching device state V2 with positive and negative water meters.""" with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "open", "waterFlowing": True, "mqttConnected": True, "wifiConnected": True, "powerSupply": "external+battery", "firmwareVersion": "2024.2.1", "uptime": 5000, "serialNumber": "abc123", "waterMeter": { "positive": {"volume": 100000, "duration": 500}, "negative": {"volume": 1000, "duration": 10} } }) device_state = await client.async_get_device_state_v2() assert device_state.valve_state == "open" assert device_state.water_flow_indicator is True assert device_state.mqtt_status is True assert device_state.wifi_status is True assert device_state.power_supply == "external+battery" assert device_state.firmware_version == "2024.2.1" assert device_state.uptime == 5000 assert device_state.serial_number == "abc123" assert device_state.water_meter_positive.volume == 100000 assert device_state.water_meter_positive.duration == 500 assert device_state.water_meter_negative.volume == 1000 assert device_state.water_meter_negative.duration == 10 @pytest.mark.asyncio async def test_get_device_state_v2_without_water_meter(client): """Test device state V2 with missing water meter data.""" with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "closed", "waterFlowing": False, "mqttConnected": False, "wifiConnected": True, "powerSupply": "battery", "firmwareVersion": "2024.1.0", "uptime": 1234, "serialNumber": "xyz789" }) device_state = await client.async_get_device_state_v2() assert device_state.valve_state == "closed" assert device_state.water_meter_positive is None assert device_state.water_meter_negative is None @pytest.mark.asyncio async def test_get_device_state_v2_network_error(client): """Test device state V2 network error handling.""" with aioresponses() as mock: mock.get("http://testserver/api/sonic/", status=500) with pytest.raises(WatergateApiException): await client.async_get_device_state_v2() @pytest.mark.asyncio async def test_injected_session(): """Test that an injected aiohttp session is used and not closed by the client.""" # Create a session to inject injected_session = aiohttp.ClientSession() try: # Create client with injected session client = WatergateLocalApiClient(base_url="http://testserver", session=injected_session) with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "open", "waterFlowing": True, "mqttConnected": True, "wifiConnected": True, "powerSupply": "external", "firmwareVersion": "2024.1.0", "waterMeter": {"volume": 100, "duration": 10}, "uptime": 1000, "serialNumber": "test123" }) device_state = await client.async_get_device_state() assert device_state.valve_state == "open" # Close the client - should NOT close the injected session await client.async_close() # Verify the injected session is still open assert not injected_session.closed finally: # Clean up the injected session await injected_session.close() @pytest.mark.asyncio async def test_owned_session_is_closed(): """Test that a client-owned session is properly closed.""" client = WatergateLocalApiClient(base_url="http://testserver") with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "closed", "waterFlowing": False, "mqttConnected": True, "wifiConnected": True, "powerSupply": "battery", "firmwareVersion": "2024.1.0", "waterMeter": {"volume": 200, "duration": 20}, "uptime": 2000, "serialNumber": "test456" }) device_state = await client.async_get_device_state() assert device_state.valve_state == "closed" # Get reference to the session before closing session = client._session assert session is not None assert not session.closed # Close the client - should close the owned session await client.async_close() # Verify the owned session is closed assert session.closed @pytest.mark.asyncio async def test_context_manager_with_injected_session(): """Test context manager doesn't close injected session.""" injected_session = aiohttp.ClientSession() try: async with WatergateLocalApiClient(base_url="http://testserver", session=injected_session) as client: with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "opening", "waterFlowing": False, "mqttConnected": True, "wifiConnected": True, "powerSupply": "external", "firmwareVersion": "2024.1.0", "waterMeter": {"volume": 300, "duration": 30}, "uptime": 3000, "serialNumber": "test789" }) device_state = await client.async_get_device_state() assert device_state.valve_state == "opening" # After exiting context, injected session should still be open assert not injected_session.closed finally: await injected_session.close() @pytest.mark.asyncio async def test_context_manager_with_owned_session(): """Test context manager closes owned session.""" session_ref = None async with WatergateLocalApiClient(base_url="http://testserver") as client: with aioresponses() as mock: mock.get("http://testserver/api/sonic/", payload={ "valveState": "closing", "waterFlowing": True, "mqttConnected": True, "wifiConnected": True, "powerSupply": "battery", "firmwareVersion": "2024.1.0", "waterMeter": {"volume": 400, "duration": 40}, "uptime": 4000, "serialNumber": "test000" }) device_state = await client.async_get_device_state() assert device_state.valve_state == "closing" session_ref = client._session # After exiting context, owned session should be closed assert session_ref is not None assert session_ref.closed