""" Real-network integration tests: mDNS, HTTP /shelly, optional subnet scan, read snapshot. Enable with: SHELLY_INTEGRATION=1 uv run pytest tests/integration -v -s Optional: SHELLY_MDNS_SECONDS=8 # browse time (default 6) SHELLY_SUBNET=192.168.1.0/24 # also run subnet scan (slow on large nets) SHELLY_SCAN_CONCURRENCY=48 SHELLY_TEST_IP=192.168.1.42 # optional :8080, or [fe80::1]:80 for IPv6 """ from __future__ import annotations import os import aiohttp import pytest from shelly_manager.api.client import ShellyRuntime, fetch_device_snapshot, probe_ip from shelly_manager.core.discovery import browse_mdns, fetch_shelly_json, scan_subnet_http from shelly_manager.core.models import DiscoveredEndpoint from tests.integration.conftest import mdns_timeout_sec, requires_integration, subnet_cidr def _parse_host_port(raw: str) -> tuple[str, int]: raw = raw.strip() if raw.startswith("["): end = raw.index("]") host = raw[1:end] rest = raw[end + 1 :] if rest.startswith(":") and rest[1:].isdigit(): return host, int(rest[1:]) return host, 80 if raw.count(":") == 1: h, p = raw.rsplit(":", 1) if p.isdigit(): return h, int(p) return raw, 80 @requires_integration @pytest.mark.integration @pytest.mark.asyncio async def test_real_mdns_browse_returns_without_error() -> None: """Browse local mDNS; must complete (list may be empty).""" timeout = mdns_timeout_sec() endpoints = await browse_mdns(timeout_sec=timeout) assert isinstance(endpoints, list) print(f"\n[integration] mDNS endpoints after {timeout}s: {len(endpoints)}") for ep in endpoints[:20]: print(f" - {ep.ip}:{ep.port} ({ep.source}) host={ep.hostname}") @requires_integration @pytest.mark.integration @pytest.mark.asyncio async def test_real_probe_mdns_endpoints() -> None: """For each discovered service, GET /shelly (non-Shelly on _http._tcp may fail probe).""" timeout = mdns_timeout_sec() endpoints = await browse_mdns(timeout_sec=timeout) if not endpoints: pytest.skip("No mDNS services (no devices or multicast blocked).") async with aiohttp.ClientSession() as session: ok = 0 for ep in endpoints: r = await probe_ip(session, ep.ip, port=ep.port) if r: gen, data = r assert isinstance(gen, int) assert isinstance(data, dict) ok += 1 print(f"\n[integration] probe OK {ep.ip}:{ep.port} gen={gen} mac={data.get('mac')}") print(f"\n[integration] successful /shelly probes: {ok}/{len(endpoints)}") @requires_integration @pytest.mark.integration @pytest.mark.asyncio async def test_real_read_snapshot_first_unauthenticated_device() -> None: """Full aioshelly read for first device that answers /shelly without auth.""" timeout = mdns_timeout_sec() endpoints = await browse_mdns(timeout_sec=timeout) if not endpoints: pytest.skip("No mDNS endpoints.") async with aiohttp.ClientSession() as session: chosen: tuple[DiscoveredEndpoint, int] | None = None for ep in endpoints: r = await probe_ip(session, ep.ip, port=ep.port) if not r: continue gen, data = r if data.get("auth") or data.get("auth_en"): print(f"\n[integration] skip auth device {ep.ip}") continue chosen = (ep, gen) break if chosen is None: for ep in endpoints: r = await probe_ip(session, ep.ip, port=ep.port) if r: chosen = (ep, r[0]) break if chosen is None: pytest.skip("No /shelly response on mDNS endpoints.") ep, gen = chosen try: async with ShellyRuntime() as runtime: snap = await fetch_device_snapshot(runtime, ep.ip, gen) except Exception as exc: # noqa: BLE001 pytest.skip(f"Snapshot read failed (network/gen/COAP): {exc}") assert snap.mac assert snap.ip == ep.ip print(f"\n[integration] snapshot OK id={snap.id} model={snap.model} gen={snap.generation}") @requires_integration @pytest.mark.integration @pytest.mark.asyncio async def test_real_subnet_scan_when_env_set() -> None: """Optional subnet scan — set SHELLY_SUBNET.""" cidr = subnet_cidr() if not cidr: pytest.skip("Set SHELLY_SUBNET e.g. 192.168.1.0/24 to run subnet scan.") conc = int(os.environ.get("SHELLY_SCAN_CONCURRENCY", "48")) async with aiohttp.ClientSession() as session: results = await scan_subnet_http( session, cidr, concurrency=min(conc, 128), ) print(f"\n[integration] subnet {cidr}: {len(results)} Shelly /shelly responses") for ip, data in results[:10]: print(f" - {ip} mac={data.get('mac')} gen={data.get('gen', '1')}") assert isinstance(results, list) @requires_integration @pytest.mark.integration @pytest.mark.asyncio async def test_real_fetch_shelly_json_direct_ip() -> None: """If SHELLY_TEST_IP is set, GET /shelly on that host:port.""" raw = os.environ.get("SHELLY_TEST_IP", "").strip() if not raw: pytest.skip("Set SHELLY_TEST_IP=192.168.1.42 or [fe80::1]:80 for direct probe.") host, port = _parse_host_port(raw) async with aiohttp.ClientSession() as session: data = await fetch_shelly_json(session, host, port=port, timeout_sec=3.0) assert data is not None assert isinstance(data, dict) print(f"\n[integration] SHELLY_TEST_IP {host}:{port} -> keys {list(data.keys())[:8]}")