From a3e5c98cd0770ac4171e0d34a9fd2d2715878197 Mon Sep 17 00:00:00 2001 From: BigBodyCobain <43977454+BigBodyCobain@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:33:01 -0600 Subject: [PATCH] test(cctv): Madrid KML HTTPS-first fallback; clarify KiwiSDR #364 docs Adds unit coverage for MadridCityIngestor catalog fetch order. Co-authored-by: Cursor --- backend/services/kiwisdr_fetcher.py | 12 ++++----- backend/tests/test_madrid_kml_https_first.py | 27 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 backend/tests/test_madrid_kml_https_first.py diff --git a/backend/services/kiwisdr_fetcher.py b/backend/services/kiwisdr_fetcher.py index 6408cc1..b046151 100644 --- a/backend/services/kiwisdr_fetcher.py +++ b/backend/services/kiwisdr_fetcher.py @@ -37,10 +37,9 @@ _SOURCE_URL_HTTPS = "https://rx.linkfanel.net/kiwisdr_com.js" _CACHE_FILE = Path(__file__).resolve().parent.parent / "data" / "kiwisdr_cache.json" # Bundled fallback — shipped with the codebase so the KiwiSDR layer always # has something to render even when the upstream is unreachable, returns -# garbage, or appears to have been tampered with. Issue #206: the upstream -# only speaks HTTP, so we can't rely on TLS for integrity — instead we -# validate the response's shape and fall back to this bundle if it doesn't -# look right. +# garbage, or appears to have been tampered with. Issue #206 / #364: try HTTPS +# first, then HTTP; we still validate shape and fall back to this bundle if the +# payload does not look right. _BUNDLED_FALLBACK = Path(__file__).resolve().parent.parent / "data" / "kiwisdr_directory.json" # Minimum number of receivers we expect from a healthy upstream response. @@ -226,9 +225,8 @@ def _load_bundled_fallback() -> list[dict]: def fetch_kiwisdr_nodes() -> list[dict]: """Return the KiwiSDR receiver list, refreshed at most once per day. - Layered fallback (issue #206 — upstream is HTTP-only, so we defend with - content validation + bundled static directory rather than trying to - upgrade the transport): + Layered fallback (issue #206 / #364 — HTTPS first, HTTP fallback, plus + content validation + bundled static directory): 1. In-memory cache (handled by @cached on this function) 2. On-disk cache if <24h old diff --git a/backend/tests/test_madrid_kml_https_first.py b/backend/tests/test_madrid_kml_https_first.py new file mode 100644 index 0000000..0d6060d --- /dev/null +++ b/backend/tests/test_madrid_kml_https_first.py @@ -0,0 +1,27 @@ +"""Madrid CCTV KML prefers HTTPS (#363).""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +from services.cctv_pipeline import MadridCityIngestor + + +def test_madrid_fetch_kml_tries_https_before_http(): + ingestor = MadridCityIngestor() + calls: list[str] = [] + + def fake_fetch(url, **kwargs): + calls.append(url) + if url == ingestor.KML_URL_HTTPS: + raise ConnectionError("tls handshake failed") + res = MagicMock() + res.status_code = 200 + res.content = b'' + res.raise_for_status = MagicMock() + return res + + with patch("services.cctv_pipeline.fetch_with_curl", side_effect=fake_fetch): + response = ingestor._fetch_kml() + + assert response.status_code == 200 + assert calls == [ingestor.KML_URL_HTTPS, ingestor.KML_URL_HTTP]