test(api): cover wifi and registry endpoints, and fix bodyless POSTs

The /wifi/* routes drive the host's real networking and the registry
routes reach GitHub, and neither had endpoint-level tests. Covering them
surfaced a bug affecting six endpoints.

Six handlers read their body as `request.get_json() or {}`. The `or {}`
says every field is optional and a missing body should fall back to
defaults — but get_json() without silent=True raises UnsupportedMediaType
when there is no JSON Content-Type, and it raises before `or {}` is ever
evaluated. Each handler's catch-all then reported that as a 500. So
POSTing with no body — what curl sends by default, and what a fetch()
without options sends — failed on /plugins/store/refresh,
/display/on-demand/start, /plugins/config/reset,
/plugins/of-the-day/json/delete, /plugins/{id}/limits and
/plugins/authenticate/spotify. The shipped UI always sends a JSON object,
which is why this stayed hidden.

All six now use silent=True. test_api_v3_optional_body.py covers the
affected endpoints and adds a source check, since the combination of
`or <default>` with a non-silent read is self-contradictory wherever it
appears and is easier to catch by inspection than by exercising each
endpoint by hand.

Also adds test/_api_v3_test_helpers.py: the blueprint holds its managers
on a module-level singleton rather than in Flask app state, so a test
that mocks them leaks into every later test unless the originals are
restored. The existing _make_client() does this for unittest classes;
this is the pytest-fixture equivalent, for the five suites still to come.

69 endpoint tests: connect/disconnect/AP/radio including the string-aware
boolean coercion these endpoints deliberately use, the radio's
lockout-refusal path, registry refresh and fetch-from-URL, and a guard
that WiFiManager is never constructed for real.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NohXi78cwsAKtN1sCfxjUh
This commit is contained in:
Claude
2026-08-13 13:50:21 +00:00
parent 54d1e314e4
commit 13dad4570a
5 changed files with 585 additions and 6 deletions
+6 -6
View File
@@ -2411,7 +2411,7 @@ def get_on_demand_status():
def start_on_demand_display():
"""Request the display controller to run a specific plugin on-demand."""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
plugin_id = data.get('plugin_id')
mode = data.get('mode')
duration = data.get('duration')
@@ -2935,7 +2935,7 @@ def manage_plugin_limits(plugin_id):
})
else:
# POST - Set limits
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
from src.plugin_system.resource_monitor import ResourceLimits
limits = ResourceLimits(
@@ -4236,7 +4236,7 @@ def refresh_plugin_store():
if not api_v3.plugin_store_manager:
return jsonify({'status': 'error', 'message': 'Plugin store manager not initialized'}), 500
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
fetch_commit_info = data.get('fetch_commit_info', data.get('fetch_latest_versions', False))
# Force refresh the registry
@@ -5822,7 +5822,7 @@ def reset_plugin_config():
if not api_v3.config_manager:
return jsonify({'status': 'error', 'message': 'Config manager not initialized'}), 500
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
plugin_id = data.get('plugin_id')
preserve_secrets = data.get('preserve_secrets', True)
@@ -6209,7 +6209,7 @@ sys.exit(proc.returncode)
def authenticate_spotify():
"""Run Spotify authentication script"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
redirect_url = data.get('redirect_url', '').strip()
# Get plugin directory
@@ -7146,7 +7146,7 @@ def upload_of_the_day_json():
def delete_of_the_day_json():
"""Delete a JSON file from of-the-day plugin"""
try:
data = request.get_json() or {}
data = request.get_json(silent=True) or {}
file_id = data.get('file_id') # This is the category_name
if not file_id: