test(url-scheme): WLED route-level integration + IPv6 regression

TestWLEDSchemeInference in test_devices_routes covers the POST/PUT
create-and-update flow with a stubbed WLED provider so the
infer_http_scheme integration hop has end-to-end coverage instead of
just the unit tests.

test_url_scheme grows public IPv6 (Cloudflare / Google / Quad9 DNS),
bracketed-form, and ULA cases. Adds an explicit pin for the Python
ipaddress documentation-prefix quirk (2001:db8::/32 is is_private,
so it routes to http:// even though some audits colloquially call it
"public").
This commit is contained in:
2026-05-23 01:13:44 +03:00
parent 0dd8d430b9
commit 907bdaf043
2 changed files with 141 additions and 0 deletions
@@ -342,6 +342,93 @@ class TestPairDevice:
assert resp.status_code == 422
class TestWLEDSchemeInference:
"""End-to-end pin for the URL-scheme inference applied to WLED devices.
The :func:`infer_http_scheme` helper has its own exhaustive unit
coverage in :file:`tests/test_url_scheme.py`; this class adds the
*integration* hop that REVIEW_TODO calls out — POST/PUT a bare host
and assert the stored device carries the inferred scheme so a future
refactor of the route handler can't quietly drop the call.
The WLED provider is stubbed out so the test does not need a real
device on the network — we just need ``validate_device`` to return a
successful payload.
"""
@pytest.fixture
def _stub_wled_validate(self, monkeypatch):
async def fake_validate(self, url): # noqa: ARG001 — provider self
return {"led_count": 30}
from ledgrab.core.devices.wled_provider import WLEDDeviceProvider
monkeypatch.setattr(WLEDDeviceProvider, "validate_device", fake_validate)
return fake_validate
def test_create_wled_with_bare_private_ip_normalises_to_http(
self, client, device_store, _stub_wled_validate
):
resp = client.post(
"/api/v1/devices",
json={
"name": "WLED desk",
"device_type": "wled",
"url": "192.168.1.42",
"led_count": 30,
},
)
assert resp.status_code == 201, resp.text
device_id = resp.json()["id"]
assert device_store.get_device(device_id).url == "http://192.168.1.42"
def test_create_wled_with_public_host_normalises_to_https(
self, client, device_store, _stub_wled_validate
):
resp = client.post(
"/api/v1/devices",
json={
"name": "WLED cloud",
"device_type": "wled",
"url": "wled.example.com",
"led_count": 30,
},
)
assert resp.status_code == 201, resp.text
device_id = resp.json()["id"]
assert device_store.get_device(device_id).url == "https://wled.example.com"
def test_create_wled_strips_trailing_slash_then_infers(
self, client, device_store, _stub_wled_validate
):
resp = client.post(
"/api/v1/devices",
json={
"name": "WLED rack",
"device_type": "wled",
"url": "wled-rack.local/",
"led_count": 30,
},
)
assert resp.status_code == 201, resp.text
device_id = resp.json()["id"]
assert device_store.get_device(device_id).url == "http://wled-rack.local"
def test_update_wled_with_bare_host_normalises_url(self, client, device_store):
existing = device_store.create_device(
name="WLED desk",
url="http://192.168.1.42",
led_count=30,
device_type="wled",
)
resp = client.put(
f"/api/v1/devices/{existing.id}",
json={"url": "10.0.0.5"},
)
assert resp.status_code == 200, resp.text
assert device_store.get_device(existing.id).url == "http://10.0.0.5"
class TestPairThenCreateFlow:
"""End-to-end coverage: pair, then persist; assert the token is
encrypted at rest and decrypted in to_config(), and that the API
+54
View File
@@ -132,3 +132,57 @@ def test_userinfo_does_not_get_https():
"""``evil.com@192.168.1.1`` must not be coerced to ``https://`` (which
would send credentials ``evil.com`` to ``192.168.1.1``)."""
assert "://" not in infer_http_scheme("evil.com@192.168.1.1")
# ---------------------------------------------------------------------------
# IPv6 literal regression — pre-hardening, the bare-label fallback split on
# the first ``:`` (``2606:4700:4700::1111`` → host="2606" → single label →
# treated as local mDNS-style) and quietly coerced public IPv6 LED targets
# to ``http://``. The bracketless ipaddress probe in ``_extract_hostname``
# fixes that; pin both the public and ULA branches so a future helper
# refactor can't regress either side.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"raw",
[
"2606:4700:4700::1111", # Cloudflare public DNS
"2001:4860:4860::8888", # Google public DNS
"2620:fe::fe", # Quad9
],
)
def test_public_ipv6_literal_gets_https(raw):
assert infer_http_scheme(raw) == f"https://{raw}"
@pytest.mark.parametrize(
"raw",
[
"[2606:4700:4700::1111]",
"[2606:4700:4700::1111]:443",
"[2001:4860:4860::8888]:8080",
],
)
def test_bracketed_public_ipv6_gets_https(raw):
assert infer_http_scheme(raw) == f"https://{raw}"
@pytest.mark.parametrize(
"raw",
[
"fc00::1", # ULA — RFC 4193
"fd12:3456:789a::1",
"fdc7::42",
],
)
def test_ula_ipv6_literal_gets_http(raw):
assert infer_http_scheme(raw) == f"http://{raw}"
def test_documentation_prefix_ipv6_treated_as_private():
"""Python's :mod:`ipaddress` classifies ``2001:db8::/32`` as private
(RFC 3849 documentation range) so the scheme inferrer reaches for the
LAN-default ``http://``. Test pins the actual behaviour rather than
the colloquial "public" label the address is sometimes given."""
assert infer_http_scheme("2001:db8::1") == "http://2001:db8::1"