mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-13 14:48:06 +00:00
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
91 lines
3.4 KiB
Python
91 lines
3.4 KiB
Python
"""
|
|
Regression tests: POST endpoints whose body is optional must accept a
|
|
request that has no body at all.
|
|
|
|
Six handlers in api_v3 read their body as ``request.get_json() or {}``.
|
|
The ``or {}`` states the intent plainly — every field is optional, so a
|
|
bodyless POST should fall back to defaults. But ``get_json()`` without
|
|
``silent=True`` raises ``UnsupportedMediaType`` when the request carries
|
|
no JSON Content-Type, and it raises *before* ``or {}`` is evaluated. Each
|
|
handler's catch-all then turned that into a 500.
|
|
|
|
So the natural way to call these endpoints — a POST with no body, which
|
|
is what curl, a fetch() without options, and most HTTP clients send by
|
|
default — failed on every one of them. The shipped UI always sends a JSON
|
|
object, which is why this went unnoticed.
|
|
|
|
This file covers the endpoints whose bodyless behaviour is not already
|
|
tested in their own suite.
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401,E402
|
|
|
|
|
|
class TestOnDemandStart:
|
|
URL = "/api/v3/display/on-demand/start"
|
|
|
|
def test_bodyless_post_is_not_a_server_error(self, api_v3_client, api_v3_module):
|
|
response = api_v3_client.post(self.URL)
|
|
# The endpoint may still reject the request on its own terms (no
|
|
# plugin_id, nothing to display); what it must not do is fail with
|
|
# a 500 raised out of body parsing.
|
|
assert response.status_code != 500
|
|
|
|
def test_json_body_still_works(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL, json={}).status_code != 500
|
|
|
|
|
|
class TestResetPluginConfig:
|
|
URL = "/api/v3/plugins/config/reset"
|
|
|
|
def test_bodyless_post_is_not_a_server_error(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL).status_code != 500
|
|
|
|
def test_json_body_still_works(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL, json={}).status_code != 500
|
|
|
|
|
|
class TestDeleteOfTheDayJson:
|
|
URL = "/api/v3/plugins/of-the-day/json/delete"
|
|
|
|
def test_bodyless_post_is_not_a_server_error(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL).status_code != 500
|
|
|
|
def test_json_body_still_works(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL, json={}).status_code != 500
|
|
|
|
|
|
class TestPluginLimits:
|
|
URL = "/api/v3/plugins/clock/limits"
|
|
|
|
def test_bodyless_post_is_not_a_server_error(self, api_v3_client, api_v3_module):
|
|
assert api_v3_client.post(self.URL).status_code != 500
|
|
|
|
|
|
class TestNoToleratedBodyReadIsUnguarded:
|
|
def test_every_or_default_body_read_uses_silent(self):
|
|
"""`get_json() or <default>` is a contradiction without silent=True.
|
|
|
|
Writing `or {}` declares the body optional; omitting silent=True
|
|
means the call raises before the default can apply. Catch the
|
|
combination here rather than waiting for each endpoint to be
|
|
exercised by hand.
|
|
"""
|
|
source = Path(__file__).parent.parent.joinpath(
|
|
"web_interface/blueprints/api_v3.py").read_text()
|
|
offenders = [
|
|
line.strip() for line in source.splitlines()
|
|
if "request.get_json()" in line and " or " in line
|
|
]
|
|
assert offenders == [], (
|
|
"these reads declare a default but raise before reaching it; "
|
|
f"use get_json(silent=True): {offenders}")
|