Skip to content

Commit 7629f62

Browse files
committed
test: close coverage gaps in linear-scan error paths
Adds tests for: - Prefix literal/anchor failures before a greedy var - Greedy scalar containing its own stop-char - Explode span not starting with separator or containing stop-chars - ifemp handling in the left-to-right prefix scan - Adjacent bounded caps in prefix (first-takes-to-stop-char) Also converts the prefix_end > suffix_start check to an assertion: _scan_prefix is bounded by suffix_start so the condition cannot hold.
1 parent ae12284 commit 7629f62

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

‎src/mcp/shared/uri_template.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -537,8 +537,9 @@ def _scan(self, uri: str) -> dict[str, str | list[str]] | None:
537537
return None
538538
prefix_result, prefix_end = prefix
539539

540-
if prefix_end > suffix_start:
541-
return None
540+
# _scan_prefix is bounded by suffix_start, so this holds by
541+
# construction. Kept as an assertion to document the invariant.
542+
assert prefix_end <= suffix_start
542543

543544
middle = uri[prefix_end:suffix_start]
544545
greedy_value = _extract_greedy(self._greedy, middle)

‎tests/shared/test_uri_template.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,49 @@ def test_match_no_backtracking_on_pathological_input(template: str, uri: str):
571571
assert UriTemplate.parse(template).match(uri) is None
572572

573573

574+
@pytest.mark.parametrize(
575+
("template", "uri"),
576+
[
577+
# Prefix literal mismatch before a greedy var
578+
("file://{+path}", "http://x"),
579+
# Prefix anchor not found: {a} needs '@' before greedy but none exists
580+
("{a}@{+path}", "no-at-sign-here"),
581+
# Prefix literal doesn't fit within suffix boundary
582+
("foo{+a}oob", "fooob"),
583+
# Greedy scalar contains its own stop-char ({+var} stops at ?)
584+
("api://{+path}", "api://foo?bar"),
585+
# Explode span doesn't start with its separator
586+
("X{/path*}", "Xnoslash"),
587+
# Explode body contains a non-separator stop-char
588+
("X{/path*}", "X/a?b"),
589+
],
590+
)
591+
def test_match_greedy_rejection_paths(template: str, uri: str):
592+
assert UriTemplate.parse(template).match(uri) is None
593+
594+
595+
@pytest.mark.parametrize(
596+
("template", "uri", "expected"),
597+
[
598+
# ifemp in prefix before a greedy var: =value form
599+
("api{;key}{+rest}", "api;key=abc/xyz", {"key": "abc", "rest": "/xyz"}),
600+
# ifemp in prefix: bare form (empty value)
601+
("api{;key}{+rest}", "api;key/xyz", {"key": "", "rest": "/xyz"}),
602+
# Adjacent bounded caps in prefix: first takes to stop-char
603+
("{a}{b}{+rest}", "foo/bar", {"a": "foo", "b": "", "rest": "/bar"}),
604+
],
605+
)
606+
def test_match_prefix_scan_edge_cases(template: str, uri: str, expected: dict[str, str]):
607+
assert UriTemplate.parse(template).match(uri) == expected
608+
609+
610+
def test_match_prefix_ifemp_rejects_name_continuation():
611+
# {;key} before a greedy var: ;keys has no = and the 's' continues
612+
# the name, so this is not our parameter.
613+
t = UriTemplate.parse("api{;key}{+rest}")
614+
assert t.match("api;keys/xyz") is None
615+
616+
574617
def test_match_large_uri_against_greedy_template():
575618
# Large payload against a greedy template — the scan visits each
576619
# character once for the suffix anchor and once for the greedy

0 commit comments

Comments
 (0)