import json import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] FIXTURES = ROOT / "tests" / "fixtures" / "http_api" FIRMWARE = ROOT / "firmware" / "esp32" / "overland-controller" / "overland-controller.ino" def load_fixture(name): return json.loads((FIXTURES / name).read_text()) def firmware_source(): return FIRMWARE.read_text() def assert_keys(payload, keys): missing = set(keys) - set(payload) assert not missing, f"missing keys: {sorted(missing)}" def registered_routes(): pattern = re.compile(r'server\.on\("([^"]+)"(?:,\s*(HTTP_[A-Z]+))?') return {(match.group(1), match.group(2) or "ANY") for match in pattern.finditer(firmware_source())} def test_firmware_registers_current_http_contract_routes(): routes = registered_routes() expected_routes = { ("/status", "ANY"), ("/config", "HTTP_GET"), ("/relay/set", "HTTP_POST"), ("/config/wifi", "HTTP_GET"), ("/config/wifi", "HTTP_POST"), ("/wifi/connect", "HTTP_POST"), ("/wifi/clear", "HTTP_POST"), ("/temps/scan", "HTTP_POST"), ("/temps/assign", "HTTP_POST"), ("/temps/clear", "HTTP_POST"), } assert expected_routes <= routes def test_status_fixture_matches_dashboard_contract_shape(): payload = load_fixture("status_response.json") assert_keys(payload, [ "type", "timestamp", "battery", "temps", "relays", "vehicle", "network", "alarms", "system", "config", ]) assert payload["type"] == "status_response" assert_keys(payload["battery"], [ "source", "connected", "soc", "voltage", "current", "remaining_ah", "capacity_ah", "runtime_hours", "temperature_f", "cycle_count", "cell_count", "ntc_count", "cell_voltages", "cell_min_voltage", "cell_max_voltage", "cell_delta_mv", "cells_valid", ]) assert_keys(payload["temps"][0], [ "id", "name", "enabled", "weather", "online", "temperature_f", ]) assert_keys(payload["relays"][0], ["id", "name", "pin", "enabled", "state"]) assert_keys(payload["network"], [ "wifi_enabled", "uart_connected", "ap_enabled", "ap_ip", "sta_enabled", "sta_connected", "sta_ssid", "sta_ip", "saved_network_count", "saved_networks", ]) assert_keys(payload["system"], [ "firmware_name", "firmware_version", "build_date", "build_time", "uptime_seconds", ]) def test_config_fixture_matches_current_config_response_shape(): payload = load_fixture("config_response.json") assert_keys(payload, ["device_name", "relays", "bms", "temperature_sensors"]) assert_keys(payload["relays"][0], ["id", "name", "pin", "enabled"]) assert_keys(payload["bms"], ["enabled", "name", "address", "address_type"]) assert_keys(payload["temperature_sensors"][0], ["id", "name", "address", "enabled"]) def test_relay_set_contract_fixtures_and_firmware_errors(): request = load_fixture("relay_set_request.json") response = load_fixture("relay_set_response.json") assert_keys(request, ["id", "state"]) assert_keys(response, ["type", "ok", "id", "state"]) assert response["type"] == "relay_response" assert response["ok"] is True source = firmware_source() assert '"invalid_json"' in source assert '"unknown_relay"' in source def test_temp_scan_assign_clear_contract_fixtures(): scan = load_fixture("temp_scan_response.json") assign_request = load_fixture("temp_assign_request.json") assign_response = load_fixture("temp_assign_response.json") clear_request = load_fixture("temp_clear_request.json") clear_response = load_fixture("temp_clear_response.json") assert_keys(scan, ["type", "ok", "devices"]) assert scan["type"] == "temp_scan_response" assert_keys(scan["devices"][0], ["index", "address"]) assert_keys(assign_request, ["id", "index"]) assert_keys(assign_response, ["device_name", "relays", "bms", "temperature_sensors"]) assert assign_response["temperature_sensors"][0]["address"] assert assign_response["temperature_sensors"][0]["enabled"] is True assert_keys(clear_request, ["id"]) assert_keys(clear_response, ["device_name", "relays", "bms", "temperature_sensors"]) assert clear_response["temperature_sensors"][0]["address"] == "" assert clear_response["temperature_sensors"][0]["enabled"] is False source = firmware_source() assert '"unknown_temp_sensor"' in source assert '"invalid_temp_selection"' in source def test_wifi_config_contract_fixtures(): request = load_fixture("wifi_config_request.json") response = load_fixture("wifi_config_response.json") assert_keys(request, ["networks"]) assert_keys(request["networks"][0], ["ssid", "password", "priority"]) assert_keys(response, ["type", "ok", "wifi"]) assert response["type"] == "wifi_config_response" assert response["ok"] is True assert_keys(response["wifi"], [ "ap_enabled", "sta_enabled", "network_count", "active_ssid", "sta_connected", "ap_ip", "sta_ip", "networks", ]) assert_keys(response["wifi"]["networks"][0], [ "index", "ssid", "priority", "password_set", ]) def test_http_error_fixture_and_known_error_codes_are_stable(): payload = load_fixture("error_response.json") source = firmware_source() assert payload == {"ok": False, "error": "invalid_json"} for error_code in [ "invalid_json", "unknown_relay", "unknown_temp_sensor", "invalid_temp_selection", "invalid_bms_selection", "invalid_relay_route", "missing_relay_action", "invalid_relay_action", ]: assert error_code in source