158 lines
5.1 KiB
Python
158 lines
5.1 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_PATH_PARAMS,
|
|
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_parameter_set_is_documented_and_minimal() -> None:
|
|
assert {"filepath", "branch", "ref", "basehead", "head", "tag"} == SLASH_PATH_PARAMS
|
|
|
|
|
|
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
|