mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-18 04:49:13 +00:00
fix(api-v3): address CodeRabbit findings from the blueprint-split review
Fixes to the api_v3 package split (PR #553), one per finding verified against the actual code: - __init__.py: _redact_credentials only blanked scalar values under a credential-named key; a bare list of secrets under such a key (e.g. tokens: ["a", "b"]) passed through untouched, since the list branch recursed with no memory that its key looked like a credential. Nested dicts still walk normally (a documented, tested behaviour -- a container like secrets: {api_key: ..., note: ...} is a section name, not a value to blank outright), but any value reached under a credential-shaped key is now actually blanked. - __init__.py: the OAuth helper script's raw stderr/stdout went to logger.error unredacted (CWE-532) right next to a comment claiming this was deliberate; the HTTP response already used the existing redact_text helper. Routed the log line through the same helper. - __init__.py / starlark.py: the standalone Starlark manifest fallback (used when the plugin instance isn't loaded) read-modified-wrote manifest.json with no lock, unlike StarlarkAppsPlugin._update_manifest_safe (plugin-repos/starlark-apps/manager.py), which already holds an flock for the same file when the plugin is loaded. Added _starlark_manifest_lock, mirroring that pattern, and wrapped every standalone read-modify-write call site in it. The app-config update route also wrote config.json and the manifest as two separate, non-transactional writes (a second, distinct finding at the same call site); config.json is now rolled back if the manifest write that follows it fails. - backup.py: restore options used bare bool() on values from the request, so {"restore_secrets": "false"} restored secrets anyway (bool("false") is True). Switched to the existing _coerce_to_bool helper already used for this exact purpose elsewhere in the package. - config.py: an automated import-rewrite mangled four user-facing validation strings and their neighbouring comments -- "Invalid start time" had become "Invalid start _pkg.time" (and likewise for "end time") in both the schedule and dim-schedule per-day validation paths. - display.py: `import _pkg.time as time_module` -- _pkg is a local alias for the package, not a real importable module, so this raised ModuleNotFoundError whenever a caller restarted an already-running display service via /display/on-demand/start, after the on-demand request was already written to cache. Fixed to `import time`. Audited the rest of the package for the same `_pkg.<module>` import mistake; every other `_pkg.` reference is a legitimate attribute read-through (`_pkg.time.time()`, `_pkg._get_starlark_plugin()`, ...), not a broken import statement. - fonts.py: validate_file_upload's max_size_mb parameter is silently unused by that helper (it only checks filename/extension) -- the font upload route saved arbitrarily large files as a result. Added the same seek-and-check pattern already used for the sibling .star upload. - wifi.py: two ad hoc, inconsistent bool coercions. POST /wifi/ap/auto-enable used bare bool(), so a JSON string "false" enabled it. POST /wifi/radio's enabled/force parsing recognized real bool and some strings but not int 1/0 (1 is True is False in Python). Factored one small _parse_bool_ish helper local to this file and used it at all three sites. Not changed: the "unknown/misspelled restore option keys default to True" half of the backup.py finding -- the file's own comment documents that a missing key deliberately means "restore everything," matching the already-existing JSON-parse-failure guard a few lines above it; only the bool-coercion defect was a real bug. Added or extended regression tests for every fix, following each area's existing test conventions. Full suite: 4328 passed, 62 skipped, 2 failed on both this branch and origin/main (missing tzdata package breaks two timezone-alias tests in test_onboarding_checklist.py, unrelated to this change) -- no new failures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S3bPMESe2TfrGvbs1ef9c5
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""Regression test: POST /fonts/upload must enforce its own stated size limit.
|
||||
|
||||
validate_file_upload(filename, max_size_mb=10, allowed_extensions=[...]) reads
|
||||
like it checks the upload's size, but it only ever validated the filename
|
||||
(traversal characters, extension) -- max_size_mb was accepted and silently
|
||||
ignored. Nothing else in the handler checked the actual upload size either,
|
||||
so it saved whatever was posted to assets/fonts/<family><ext> regardless of
|
||||
size, unlike the sibling .star and plugin-asset upload routes, which check
|
||||
`file.tell()` against a stated limit before saving.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
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
|
||||
|
||||
URL = "/api/v3/fonts/upload"
|
||||
TEN_MB = 10 * 1024 * 1024
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fonts_root(tmp_path):
|
||||
# PROJECT_ROOT is bound by value in fonts.py, so it is patched there.
|
||||
with patch("web_interface.blueprints.api_v3.fonts.PROJECT_ROOT", tmp_path):
|
||||
yield tmp_path
|
||||
|
||||
|
||||
def upload(client, content, filename="myfont.ttf", family="myfont"):
|
||||
data = {
|
||||
"font_file": (io.BytesIO(content), filename),
|
||||
"font_family": family,
|
||||
}
|
||||
return client.post(URL, data=data, content_type="multipart/form-data")
|
||||
|
||||
|
||||
class TestFontUploadSizeLimit:
|
||||
def test_oversized_font_is_rejected(self, api_v3_client, fonts_root):
|
||||
response = upload(api_v3_client, b"x" * (TEN_MB + 1))
|
||||
assert response.status_code == 400
|
||||
assert "too large" in response.get_json()["message"].lower()
|
||||
assert not (fonts_root / "assets" / "fonts" / "myfont.ttf").exists(), (
|
||||
"an oversized font was saved to disk before being rejected")
|
||||
|
||||
def test_a_font_right_at_the_limit_is_accepted(self, api_v3_client, fonts_root):
|
||||
response = upload(api_v3_client, b"x" * TEN_MB,
|
||||
filename="atlimit.ttf", family="atlimit")
|
||||
assert response.status_code == 200, response.get_json()
|
||||
assert (fonts_root / "assets" / "fonts" / "atlimit.ttf").exists()
|
||||
|
||||
def test_an_ordinary_small_font_is_still_accepted(self, api_v3_client, fonts_root):
|
||||
response = upload(api_v3_client, b"fake font bytes",
|
||||
filename="small.ttf", family="small")
|
||||
assert response.status_code == 200, response.get_json()
|
||||
assert (fonts_root / "assets" / "fonts" / "small.ttf").read_bytes() == b"fake font bytes"
|
||||
Reference in New Issue
Block a user