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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user