Files
AegisGitea-MCP/tests/test_executor_units.py
T
Latte 156e963e21 fix: key slash encoding on route, redact more request secrets, bound JSON depth and uploads
Slash safety is decided per (path template, parameter) so single-segment
routes such as releases/tags/{tag} and editorconfig/{filepath} keep %2F.
Adds authorization_header, remote_password, mirror_password, mirror_token and
the AWS key fields to the redacted request fields. Deeply nested JSON from
Gitea is treated as text instead of raising RecursionError, and base64
uploads are capped before decoding.
2026-10-14 16:08:42 +02:00

166 lines
5.2 KiB
Python

"""Unit tests for the executor's pure helpers (encoding, pagination, redaction)."""
from __future__ import annotations
import pytest
from aegis_gitea_mcp.gitea.encoding import (
SLASH_ROUTES,
ArgumentError,
check_segments,
render_path,
render_raw_path,
sensitive_body_values,
validate_sudo,
)
from aegis_gitea_mcp.gitea.pagination import build_pagination, parse_link_header
from aegis_gitea_mcp.gitea.redaction import normalize_secrets, redact_exact, redact_text
from tests.test_executor_helpers import GET_BRANCH, GET_RAW_FILE, PULL_DIFF
# Encoding
def test_slash_routes_are_the_verified_wildcard_routes() -> None:
assert {name for _, name in SLASH_ROUTES} == {
"filepath",
"branch",
"ref",
"basehead",
"head",
"tag",
}
assert len(SLASH_ROUTES) == 11
def test_render_path_returns_encoded_and_display_forms() -> None:
encoded, display = render_path(
GET_RAW_FILE, {"owner": "o", "repo": "r", "filepath": "a b/c#"[:-1]}
)
assert encoded == "/repos/o/r/raw/a%20b/c"
assert display == "/repos/o/r/raw/a b/c"
def test_render_path_repeated_use_of_integers() -> None:
encoded, _ = render_path(PULL_DIFF, {"owner": "o", "repo": "r", "index": 3, "diffType": "diff"})
assert encoded == "/repos/o/r/pulls/3.diff"
def test_branch_with_slashes_is_not_quoted_but_other_chars_are() -> None:
encoded, _ = render_path(GET_BRANCH, {"owner": "o", "repo": "r", "branch": "fix/a+b c"})
assert encoded.endswith("/branches/fix/a%2Bb%20c")
@pytest.mark.parametrize("path", ["/user", "/repos/o/r/issues", "/a/b.c/d-e_f"])
def test_raw_path_accepts_plain_paths(path: str) -> None:
assert render_raw_path(path) == (path, path)
def test_raw_path_encodes_non_ascii_but_displays_it_raw() -> None:
assert render_raw_path("/repos/ö/r") == ("/repos/%C3%B6/r", "/repos/ö/r")
@pytest.mark.parametrize("value", ["a", "a b", "ünï", "a.b", "a..b", ".hidden", "-x"])
def test_check_segments_accepts(value: str) -> None:
check_segments(value, "v")
@pytest.mark.parametrize("value", ["", ".", "..", "a/./b", "a/../b", "/a", "a/", "a\\b", "a%b"])
def test_check_segments_rejects(value: str) -> None:
with pytest.raises(ArgumentError):
check_segments(value, "v")
def test_validate_sudo() -> None:
assert validate_sudo(None) is None
assert validate_sudo("Bob_1.x-y") == "Bob_1.x-y"
with pytest.raises(ArgumentError):
validate_sudo("bob@example.com")
def test_sensitive_body_values_variants() -> None:
assert sensitive_body_values({"data": 5}, secret_data=True) == []
assert sensitive_body_values({"data": ""}, secret_data=True) == []
assert sensitive_body_values("text", secret_data=True) == []
assert sensitive_body_values({"data": "x"}, secret_data=False) == []
assert sensitive_body_values({"data": "xy"}, secret_data=True) == ["xy"]
assert sensitive_body_values({"PassWord": "abcd", "n": 1}, secret_data=False) == ["abcd"]
assert sensitive_body_values({"password": "abc"}, secret_data=False) == []
assert sensitive_body_values({"password": 12345}, secret_data=False) == []
# Pagination
def test_parse_link_header() -> None:
header = (
'<https://g/api?page=2&limit=5>; rel="next", <https://g/api?page=9>; rel=last, '
'<https://g/api?page=x&limit=0>; rel="prev first", <https://g/api?page=3>; title="t"'
)
links = parse_link_header(header)
assert links["next"] == {"page": 2, "limit": 5}
assert links["last"] == {"page": 9, "limit": None}
assert links["prev"] == {"page": None, "limit": None}
assert links["first"] == links["prev"]
assert len(links) == 4
assert parse_link_header(None) == {}
assert parse_link_header("garbage") == {}
def test_build_pagination_nothing_to_report() -> None:
assert build_pagination({}, requested_page=None, requested_limit=None, paginated=False) is None
def test_build_pagination_limit_from_links_and_garbage_total() -> None:
result = build_pagination(
{"link": '<https://g/?page=2&limit=7>; rel="last"', "x-total-count": "many"},
requested_page="x",
requested_limit=None,
paginated=True,
)
assert result == {
"page": 1,
"limit": 7,
"total_count": None,
"next_page": None,
"last_page": 2,
}
def test_build_pagination_page_derived_from_next_of_two() -> None:
result = build_pagination(
{"link": '<https://g/?page=1>; rel="next"'},
requested_page=None,
requested_limit=None,
paginated=True,
)
assert result is not None
assert result["page"] == 1
# Redaction
def test_normalize_secrets_orders_longest_first_and_drops_empty() -> None:
assert normalize_secrets(["ab", None, "", "abcd", "ab"]) == ("abcd", "ab")
def test_redact_text_prefers_longer_secret() -> None:
assert redact_text("xx abcd ab", ("abcd", "ab")) == "xx [REDACTED] [REDACTED]"
def test_redact_exact_covers_all_container_types() -> None:
value = {"k-sec": ("sec", ["sec", 1, None, True]), "n": 2.5}
assert redact_exact(value, ("sec",)) == {
"k-[REDACTED]": ["[REDACTED]", ["[REDACTED]", 1, None, True]],
"n": 2.5,
}
assert redact_exact(value, ()) is value