Add HTTP API contract fixtures

This commit is contained in:
2026-06-07 10:15:33 -06:00
parent d9b851aa53
commit ec39182bbd
15 changed files with 448 additions and 0 deletions
+1
View File
@@ -23,6 +23,7 @@ COMMANDS = [
"pico-dashboard/hardware/touch.py",
"pico-dashboard/hardware/buzzer.py"],
[sys.executable, "-m", "pytest", "tests/test_pico_core.py"],
[sys.executable, "-m", "pytest", "tests/test_http_api_contract.py"],
]
+1
View File
@@ -1 +1,2 @@
flask
pytest
+31
View File
@@ -0,0 +1,31 @@
{
"device_name": "Cargo Controller",
"relays": [
{
"id": "relay_1",
"name": "Aux Power",
"pin": 16,
"enabled": true
},
{
"id": "relay_2",
"name": "Fridge",
"pin": 17,
"enabled": true
}
],
"bms": {
"enabled": true,
"name": "House Battery",
"address": "aa:bb:cc:dd:ee:ff",
"address_type": "public"
},
"temperature_sensors": [
{
"id": "temp_1",
"name": "Cabin",
"address": "28ff001122334455",
"enabled": true
}
]
}
+4
View File
@@ -0,0 +1,4 @@
{
"ok": false,
"error": "invalid_json"
}
+4
View File
@@ -0,0 +1,4 @@
{
"id": "relay_1",
"state": true
}
+6
View File
@@ -0,0 +1,6 @@
{
"type": "relay_response",
"ok": true,
"id": "relay_1",
"state": true
}
+112
View File
@@ -0,0 +1,112 @@
{
"type": "status_response",
"timestamp": 123456,
"battery": {
"source": "jbd_bms",
"connected": true,
"soc": 70,
"voltage": 13.34,
"current": -5.2,
"remaining_ah": 104.8,
"capacity_ah": 150.0,
"runtime_hours": 20.15,
"temperature_f": 76.5,
"cycle_count": 3,
"cell_count": 4,
"ntc_count": 2,
"cell_voltages": [3.334, 3.333, 3.334, 3.333],
"cell_min_voltage": 3.333,
"cell_max_voltage": 3.334,
"cell_delta_mv": 1,
"cells_valid": true
},
"temps": [
{
"id": "temp_1",
"name": "Cabin",
"enabled": true,
"weather": false,
"online": true,
"temperature_f": 72.4
}
],
"relays": [
{
"id": "relay_1",
"name": "Aux Power",
"pin": 16,
"enabled": true,
"state": false
},
{
"id": "relay_2",
"name": "Fridge",
"pin": 17,
"enabled": true,
"state": true
}
],
"vehicle": {
"ignition_on": false
},
"network": {
"wifi_enabled": true,
"uart_connected": true,
"ap_enabled": true,
"ap_ip": "192.168.4.1",
"sta_enabled": true,
"sta_connected": false,
"sta_ssid": "",
"sta_ip": "",
"saved_network_count": 1,
"saved_networks": [
{
"index": 1,
"ssid": "Starlink",
"priority": 1,
"active": false
}
]
},
"alarms": {
"low_soc": false,
"critical_soc": false,
"low_voltage": false,
"high_battery_temp": false,
"cell_imbalance": false,
"bms_disconnected": false
},
"system": {
"firmware_name": "overland-controller",
"firmware_version": "0.4.0",
"build_date": "Jun 07 2026",
"build_time": "12:00:00",
"uptime_seconds": 42
},
"config": {
"device_name": "Cargo Controller",
"relays": [
{
"id": "relay_1",
"name": "Aux Power",
"pin": 16,
"enabled": true
}
],
"bms": {
"enabled": true,
"name": "House Battery",
"address": "aa:bb:cc:dd:ee:ff",
"address_type": "public"
},
"temperature_sensors": [
{
"id": "temp_1",
"name": "Cabin",
"address": "28ff001122334455",
"enabled": true,
"weather": false
}
]
}
}
+4
View File
@@ -0,0 +1,4 @@
{
"id": "temp_1",
"index": 1
}
+18
View File
@@ -0,0 +1,18 @@
{
"device_name": "Cargo Controller",
"relays": [],
"bms": {
"enabled": true,
"name": "House Battery",
"address": "aa:bb:cc:dd:ee:ff",
"address_type": "public"
},
"temperature_sensors": [
{
"id": "temp_1",
"name": "Cabin",
"address": "28ff001122334455",
"enabled": true
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"id": "temp_1"
}
+18
View File
@@ -0,0 +1,18 @@
{
"device_name": "Cargo Controller",
"relays": [],
"bms": {
"enabled": true,
"name": "House Battery",
"address": "aa:bb:cc:dd:ee:ff",
"address_type": "public"
},
"temperature_sensors": [
{
"id": "temp_1",
"name": "Cabin",
"address": "",
"enabled": false
}
]
}
+10
View File
@@ -0,0 +1,10 @@
{
"type": "temp_scan_response",
"ok": true,
"devices": [
{
"index": 1,
"address": "28ff001122334455"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
{
"networks": [
{
"ssid": "Starlink",
"password": "example-password",
"priority": 1
}
]
}
+21
View File
@@ -0,0 +1,21 @@
{
"type": "wifi_config_response",
"ok": true,
"wifi": {
"ap_enabled": true,
"sta_enabled": true,
"network_count": 1,
"active_ssid": "",
"sta_connected": false,
"ap_ip": "192.168.4.1",
"sta_ip": "",
"networks": [
{
"index": 1,
"ssid": "Starlink",
"priority": 1,
"password_set": true
}
]
}
}
+206
View File
@@ -0,0 +1,206 @@
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