"""InfluxDB check: query text, CSV parsing, classification, failures (no network).""" import datetime as dt import urllib.error from kota_tech.diag.guides import INFLUX_OUTCOMES from kota_tech.service import influx NOW = dt.datetime(2026, 9, 30, 12, 0, tzinfo=dt.timezone.utc) UTC = dt.timezone.utc # Real answer shape of /api/v2/query (Accept: application/csv). CSV = ( ",result,table,_measurement,_time\r\n" ",_result,0,as7341_adc,2026-09-30T11:55:01Z\r\n" ",_result,0,bme688,2026-09-30T11:55:02.123456789Z\r\n" ",_result,0,device_status,2026-09-30T11:58:00Z\r\n" ",_result,0,microphone,2026-09-30T11:58:10Z\r\n" ",_result,0,sen66,2026-09-30T11:56:00Z\r\n" "\r\n" ) def _t(minutes_ago): return NOW - dt.timedelta(minutes=minutes_ago) def test_outcomes_match_guide_runner(): assert influx.OUTCOMES == INFLUX_OUTCOMES def test_query_is_for_one_box_and_keeps_only_time(): q = influx.build_query("sensorbox", "A4CB8FC9473C") assert 'from(bucket: "sensorbox")' in q and 'r.device == "A4CB8FC9473C"' in q # _value must be dropped before grouping (string and float fields clash -> HTTP 400) assert q.index("keep(") < q.index("group(columns") def test_parse_csv_newest_per_measurement(): latest = influx.parse_csv(CSV) assert latest["bme688"] == dt.datetime(2026, 9, 30, 11, 55, 2, 123456, tzinfo=UTC) assert set(latest) == {"as7341_adc", "bme688", "device_status", "microphone", "sen66"} assert influx.parse_csv("") == {} def test_all_sensors_fresh_is_ok(): c = influx.classify(influx.parse_csv(CSV), NOW) assert c.outcome == influx.OK and not c.missing def test_status_without_sensors_is_sensors_missing(): # box A4CB8FC9473C: RTC stopped, only status and microphone ever arrived c = influx.classify({"device_status": _t(1), "microphone": _t(1)}, NOW) assert c.outcome == influx.SENSORS_MISSING assert c.missing == list(influx.SENSOR_GROUPS) assert any("never started" in d for d in c.details) def test_one_sensor_missing(): latest = {"device_status": _t(1), "as7341_adc": _t(2), "bme688": _t(2), "sen66": _t(300)} c = influx.classify(latest, NOW) assert c.outcome == influx.SENSORS_MISSING and c.missing == ["SEN66 (air quality)"] def test_offline_box_whose_sensors_stopped_before_status(): # box A4CB8FC946F8: status 29 h ago, sensors 21 days ago latest = {"device_status": _t(29 * 60), "microphone": _t(29 * 60), "as7341_adc": _t(21 * 1440), "bme688": _t(21 * 1440), "sen66": _t(21 * 1440)} c = influx.classify(latest, NOW) assert c.outcome == influx.SENSORS_MISSING and "offline now" in c.summary assert any("stopped" in d for d in c.details) assert any("21 days ago" in d for d in c.details) def test_offline_box_with_complete_history_is_silent(): # box A4CB8FC739EC: everything stopped together 11 days ago latest = {"device_status": _t(11 * 1440), "microphone": _t(11 * 1440), "as7341_adc": _t(11 * 1440 + 3), "bme688": _t(11 * 1440 + 3), "sen66": _t(11 * 1440 + 2)} c = influx.classify(latest, NOW) assert c.outcome == influx.SILENT assert not any("stopped" in d for d in c.details) def test_no_data_is_never(): assert influx.classify({}, NOW).outcome == influx.NEVER def test_future_timestamps_are_flagged(): latest = {"device_status": _t(1), "microphone": _t(1), "as7341_adc": _t(-120), "bme688": _t(-120), "sen66": _t(-120)} c = influx.classify(latest, NOW) assert any("future" in d for d in c.details) def test_check_device_without_token_or_mac(): assert influx.check_device(None, "A4CB8FC9473C").outcome == influx.UNAVAILABLE cfg = influx.InfluxConfig("t") assert influx.check_device(cfg, "").outcome == influx.UNAVAILABLE def test_check_device_errors_never_raise(monkeypatch): cfg = influx.InfluxConfig("secret-token") def rejected(*_a, **_k): raise urllib.error.HTTPError("u", 401, "Unauthorized", {}, None) monkeypatch.setattr(influx.urllib.request, "urlopen", rejected) c = influx.check_device(cfg, "a4:cb:8f:c9:47:3c") assert c.outcome == influx.UNAVAILABLE and "rejected" in c.summary assert "secret-token" not in c.summary def offline(*_a, **_k): raise urllib.error.URLError("nodename nor servname provided") monkeypatch.setattr(influx.urllib.request, "urlopen", offline) assert "not reachable" in influx.check_device(cfg, "A4CB8FC9473C").summary def test_check_device_sends_token_header_and_normalized_mac(monkeypatch): seen = {} class Resp: def __enter__(self): return self def __exit__(self, *a): return False def read(self): return CSV.encode() def fake_urlopen(req, timeout): seen["req"] = req return Resp() monkeypatch.setattr(influx.urllib.request, "urlopen", fake_urlopen) c = influx.check_device(influx.InfluxConfig("tok"), "a4:cb:8f:c9:47:3c", now=NOW) req = seen["req"] assert c.outcome == influx.OK assert req.get_header("Authorization") == "Token tok" assert "tok" not in req.full_url and req.full_url.endswith("/api/v2/query?org=kota") assert b'"A4CB8FC9473C"' in req.data