mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-14 10:37:23 +00:00
0ad95cb16a
release: v0.50.241
Batch release of 4 PRs:
- #1290 (@nickgiulioni1) — Inline audio/video media editor with playback
speed controls and HTTP byte-range streaming. PDF/media previews in
workspace file browser. Composer tray inline players for audio/video.
(Rebased from #1232.)
- #1287 (@renatomott) — Configured model badges (Primary / Fallback N) in
the model picker, carried through to the composer chip. Persists through
on-disk model cache.
- #1289 (@franksong2702) — Appearance autosave for theme/skin/font-size in
Settings; inline Saving / Saved / Failed status. Font size now persists
to config.yaml. Refs #1003.
- #1294 (@franksong2702) — Normalize agent session source metadata
(raw_source / session_source / source_label) through /api/sessions and
gateway watcher SSE snapshots. Existing source_tag / is_cli_session
fields preserved. Refs #1013.
Tests: 3254 passed, 2 skipped, 3 xpassed (was 3199 before this release).
Independently reviewed and approved by nesquena (commit d1738f6).
88 lines
4.2 KiB
Python
88 lines
4.2 KiB
Python
"""Edge-case unit tests for _parse_range_header (PR #1290).
|
|
|
|
The byte-range parser is security-relevant — malformed Range headers from
|
|
clients can cause off-by-one bugs, integer overflows, or info disclosure
|
|
if not handled correctly per RFC 7233. The PR adds higher-level tests
|
|
for media inline streaming, but the range parser itself has no direct
|
|
unit tests. This file pins the parser's contract.
|
|
"""
|
|
import pytest
|
|
|
|
from api.routes import _parse_range_header
|
|
|
|
|
|
# Each tuple: (header, file_size, expected_result)
|
|
# expected_result is None for invalid/unsatisfiable, or (start, end) inclusive.
|
|
RANGE_CASES = [
|
|
# ── Valid ranges ─────────────────────────────────────────────────────
|
|
("bytes=0-99", 1000, (0, 99), "explicit start-end"),
|
|
("bytes=0-", 1000, (0, 999), "open-ended start"),
|
|
("bytes=100-", 1000, (100, 999), "open-ended from middle"),
|
|
("bytes=-500", 1000, (500, 999), "suffix range — last 500 bytes"),
|
|
("bytes=-99999", 1000, (0, 999), "suffix > file_size clamps to start=0"),
|
|
("bytes=0-99999", 1000, (0, 999), "end > file_size clamps to file_size-1"),
|
|
("bytes=999-1500", 1000, (999, 999), "end past file clamps to last byte"),
|
|
("bytes=100-100", 1000, (100, 100), "single-byte range"),
|
|
("bytes=999-999", 1000, (999, 999), "last byte"),
|
|
("bytes= 0-99", 1000, (0, 99), "whitespace inside trimmed"),
|
|
# ── Invalid → None (caller sends 416 Range Not Satisfiable) ─────────
|
|
("", 1000, None, "empty header"),
|
|
("bytes 0-100", 1000, None, "wrong format — space instead of ="),
|
|
("bytes=", 1000, None, "no spec after ="),
|
|
("bytes=-", 1000, None, "bare dash, no numbers"),
|
|
("bytes=-0", 1000, None, "zero-length suffix"),
|
|
("bytes=0-99,200-299", 1000, None, "multipart not supported"),
|
|
("bytes=500-100", 1000, None, "reversed range"),
|
|
("bytes=999999-", 1000, None, "start past file"),
|
|
("bytes=abc-def", 1000, None, "non-numeric"),
|
|
("bytes=-abc", 1000, None, "non-numeric suffix"),
|
|
(" bytes=0-99", 1000, None, "leading space — must startswith bytes="),
|
|
# ── Empty/zero file ─────────────────────────────────────────────────
|
|
("bytes=0-99", 0, None, "empty file always None"),
|
|
# ── Negative numbers (should not yield negative offsets) ────────────
|
|
("bytes=-1", 1000, (999, 999), "suffix=1 — last byte"),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"header,file_size,expected,description",
|
|
RANGE_CASES,
|
|
ids=[c[3] for c in RANGE_CASES],
|
|
)
|
|
def test_parse_range_header(header, file_size, expected, description):
|
|
actual = _parse_range_header(header, file_size)
|
|
assert actual == expected, (
|
|
f"_parse_range_header({header!r}, {file_size}) = {actual!r}, "
|
|
f"expected {expected!r} — {description}"
|
|
)
|
|
|
|
|
|
# ── Invariants beyond the case table ────────────────────────────────────
|
|
|
|
|
|
def test_returned_offsets_are_non_negative_for_valid_inputs():
|
|
for header, file_size, expected, _ in RANGE_CASES:
|
|
if expected is None:
|
|
continue
|
|
start, end = _parse_range_header(header, file_size)
|
|
assert start >= 0, f"start must be non-negative: {start} from {header!r}"
|
|
assert end >= start, f"end must be >= start: ({start},{end}) from {header!r}"
|
|
assert end < file_size, (
|
|
f"end must be < file_size for valid range: {end} >= {file_size} "
|
|
f"from {header!r}"
|
|
)
|
|
|
|
|
|
def test_content_length_is_positive_for_valid_ranges():
|
|
"""The (start, end) returned must always describe at least one byte —
|
|
otherwise _serve_file_bytes would compute Content-Length=0 incorrectly."""
|
|
for header, file_size, expected, _ in RANGE_CASES:
|
|
if expected is None:
|
|
continue
|
|
start, end = _parse_range_header(header, file_size)
|
|
content_length = end - start + 1
|
|
assert content_length >= 1, (
|
|
f"Valid range must have positive content-length, got {content_length} "
|
|
f"from {header!r} on size {file_size}"
|
|
)
|