mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-07 07:36:37 +00:00
Whole-tree audit. Every symbol was checked against core, the plugin monorepo and all eight third-party plugins in plugins.json first. - Deprecate (removal 3.10.0) plugin-facing methods nothing calls: LogoDownloader bulk download, ConfigManager backup/secret wrappers, APIHelper extras, BackgroundDataService poll API, PluginManager / PluginStateManager info readers, and a few CacheManager, FontManager, BaseOddsManager, DynamicTeamResolver methods and PluginTestCase. plugin_api_usage.py learns their receiver names; DEPRECATIONS doc regenerated. - Remove core-internal dead code: CacheMetrics, Vegas status/stats plumbing, sync "new cycle" message (followers ignore unknown types), unused operation types, test-only PluginCatalog readers, IPC to_dict and ping, _parse_form_value, CacheStrategyProtocol, ErrorAggregator callbacks, duplicate web response helpers. - Web UI: drop never-mounted json-file-manager.js, the example widget, utils/error_handler.js, four uncalled PluginAPI methods, and 29 escapeHtml shims (call window.LEDEscape directly). Public globals, BaseWidget and widget names unchanged. - Remove six one-off scripts (owner decision) and the unused markupsafe and pytest-mock pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
8.2 KiB
Python
207 lines
8.2 KiB
Python
"""
|
|
Tests for the response builders in src/web_interface/api_helpers.py.
|
|
|
|
describe_exception() in error_handler.py is already covered by
|
|
test/test_web_error_detail.py and is not duplicated here.
|
|
|
|
Regression coverage for one fixed bug: the success builder used
|
|
truthiness for `message` and `metadata` while using `is not None` for
|
|
`data`, so an explicitly-passed "" or {} was silently dropped —
|
|
success_response() repeated the same gate, which is the path every api_v3
|
|
endpoint actually calls.
|
|
"""
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
from src.web_interface.api_helpers import (
|
|
error_response,
|
|
exception_error_response,
|
|
success_response,
|
|
)
|
|
from src.web_interface.errors import ErrorCode, WebInterfaceError
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
return Flask(__name__)
|
|
|
|
|
|
class TestErrorResponse:
|
|
def test_returns_response_and_status_tuple(self, app):
|
|
with app.test_request_context():
|
|
response, status = error_response(
|
|
ErrorCode.CONFIG_SAVE_FAILED, "could not save")
|
|
assert status == 500
|
|
assert response.get_json()["message"] == "could not save"
|
|
|
|
def test_status_code_passthrough(self, app):
|
|
with app.test_request_context():
|
|
_, status = error_response(
|
|
ErrorCode.INVALID_INPUT, "bad", status_code=400)
|
|
assert status == 400
|
|
|
|
def test_body_matches_the_error_dataclass(self, app):
|
|
with app.test_request_context():
|
|
response, _ = error_response(
|
|
ErrorCode.NETWORK_ERROR, "offline",
|
|
details="connection refused", context={"url": "http://x"})
|
|
expected = WebInterfaceError(
|
|
error_code=ErrorCode.NETWORK_ERROR, message="offline",
|
|
details="connection refused", context={"url": "http://x"}).to_dict()
|
|
assert response.get_json() == expected
|
|
|
|
def test_none_context_produces_no_context_key(self, app):
|
|
with app.test_request_context():
|
|
response, _ = error_response(ErrorCode.SYSTEM_ERROR, "boom")
|
|
assert "context" not in response.get_json()
|
|
|
|
def test_suggested_fixes_passed_through(self, app):
|
|
with app.test_request_context():
|
|
response, _ = error_response(
|
|
ErrorCode.SYSTEM_ERROR, "boom", suggested_fixes=["Try again"])
|
|
assert response.get_json()["suggested_fixes"] == ["Try again"]
|
|
|
|
|
|
class TestExceptionErrorResponse:
|
|
"""The one-call form of from_exception() + error_response().
|
|
|
|
Nine plugin routes spelled the pair out by hand; these pin that the helper
|
|
answers exactly what that spelling did, so folding them changed nothing a
|
|
client sees.
|
|
"""
|
|
|
|
@staticmethod
|
|
def _by_hand(exc, code, with_context):
|
|
error = WebInterfaceError.from_exception(exc, code)
|
|
if with_context:
|
|
return error_response(error.error_code, error.message,
|
|
details=error.details, context=error.context,
|
|
status_code=500)
|
|
return error_response(error.error_code, error.message,
|
|
details=error.details, status_code=500)
|
|
|
|
@pytest.mark.parametrize("with_context", [True, False])
|
|
@pytest.mark.parametrize("code", [ErrorCode.SYSTEM_ERROR,
|
|
ErrorCode.CONFIG_SAVE_FAILED,
|
|
ErrorCode.PLUGIN_UPDATE_FAILED])
|
|
def test_same_answer_as_the_hand_written_pair(self, app, code, with_context):
|
|
exc = ValueError("token=SECRET boom")
|
|
exc.context = {"config_path": "/etc/x.json"}
|
|
with app.test_request_context():
|
|
got, got_status = exception_error_response(
|
|
exc, code, with_context=with_context)
|
|
want, want_status = self._by_hand(exc, code, with_context)
|
|
assert got_status == want_status == 500
|
|
assert got.get_json() == want.get_json()
|
|
|
|
def test_shape(self, app):
|
|
with app.test_request_context():
|
|
response, status = exception_error_response(
|
|
RuntimeError("token=SECRET"), ErrorCode.SYSTEM_ERROR)
|
|
assert status == 500
|
|
assert response.get_json() == {
|
|
"status": "error",
|
|
"error_code": "SYSTEM_ERROR",
|
|
"message": "A system error occurred",
|
|
"context": {"exception_type": "RuntimeError"},
|
|
"suggested_fixes": ["Review error details and try again"],
|
|
}
|
|
|
|
def test_without_context(self, app):
|
|
with app.test_request_context():
|
|
response, _ = exception_error_response(
|
|
RuntimeError("x"), ErrorCode.SYSTEM_ERROR, with_context=False)
|
|
assert "context" not in response.get_json()
|
|
|
|
|
|
def _success_body(**kwargs):
|
|
"""success_response()'s body, outside any request timing."""
|
|
with Flask(__name__).test_request_context():
|
|
return success_response(**kwargs).get_json()
|
|
|
|
|
|
class TestSuccessResponseBody:
|
|
def test_bare_success(self):
|
|
assert _success_body() == {"status": "success"}
|
|
|
|
def test_data_included(self):
|
|
assert _success_body(data={"a": 1})["data"] == {"a": 1}
|
|
|
|
@pytest.mark.parametrize("falsy", [0, "", False, {}, []])
|
|
def test_falsy_data_is_still_included(self, falsy):
|
|
assert _success_body(data=falsy)["data"] == falsy
|
|
|
|
def test_none_data_omitted(self):
|
|
assert "data" not in _success_body(data=None)
|
|
|
|
def test_message_included(self):
|
|
assert _success_body(message="done")["message"] == "done"
|
|
|
|
def test_empty_message_is_still_included(self):
|
|
# Regression: `if message:` dropped an explicitly-passed "".
|
|
assert _success_body(message="")["message"] == ""
|
|
|
|
def test_none_message_omitted(self):
|
|
assert "message" not in _success_body(message=None)
|
|
|
|
def test_metadata_included(self):
|
|
assert _success_body(metadata={"v": 1})["metadata"] == {"v": 1}
|
|
|
|
def test_empty_metadata_is_still_included(self):
|
|
# Regression: `if metadata:` dropped an explicitly-passed {}.
|
|
assert _success_body(metadata={})["metadata"] == {}
|
|
|
|
def test_none_metadata_omitted(self):
|
|
assert "metadata" not in _success_body(metadata=None)
|
|
|
|
|
|
class TestSuccessResponseHelper:
|
|
"""api_helpers.success_response — the wrapper every endpoint calls."""
|
|
|
|
def test_plain_response_has_no_metadata_block(self, app):
|
|
with app.test_request_context():
|
|
body = success_response(data={"a": 1}).get_json()
|
|
assert body == {"status": "success", "data": {"a": 1}}
|
|
|
|
def test_explicit_empty_metadata_survives_the_wrapper(self, app):
|
|
# Regression: the wrapper re-gated metadata on truthiness after
|
|
# the body builder had already included it, so {} was dropped
|
|
# again on the way out.
|
|
with app.test_request_context():
|
|
body = success_response(data=None, metadata={}).get_json()
|
|
assert body["metadata"] == {}
|
|
|
|
def test_caller_metadata_preserved(self, app):
|
|
with app.test_request_context():
|
|
body = success_response(metadata={"version": "1.2"}).get_json()
|
|
assert body["metadata"]["version"] == "1.2"
|
|
|
|
def test_timing_added_when_request_has_start_time(self, app):
|
|
with app.test_request_context() as ctx:
|
|
ctx.request.start_time = 0.0
|
|
body = success_response(data={"a": 1}).get_json()
|
|
assert "response_time_ms" in body["metadata"]
|
|
|
|
def test_timing_merges_with_caller_metadata(self, app):
|
|
with app.test_request_context() as ctx:
|
|
ctx.request.start_time = 0.0
|
|
body = success_response(metadata={"version": "1.2"}).get_json()
|
|
assert body["metadata"]["version"] == "1.2"
|
|
assert "response_time_ms" in body["metadata"]
|
|
|
|
def test_caller_metadata_dict_is_not_mutated(self, app):
|
|
# The helper used to add response_time_ms straight into the dict the
|
|
# caller passed, so a module-level or reused metadata dict would
|
|
# accumulate timings from previous requests.
|
|
caller_metadata = {"version": "1.2"}
|
|
with app.test_request_context() as ctx:
|
|
ctx.request.start_time = 0.0
|
|
success_response(metadata=caller_metadata)
|
|
assert caller_metadata == {"version": "1.2"}
|
|
|
|
def test_message_passed_through(self, app):
|
|
with app.test_request_context():
|
|
body = success_response(message="saved").get_json()
|
|
assert body["message"] == "saved"
|