Files
ss-tools/backend/tests/services/git/test_git_url.py
busya 997329e2a5 fix(agent): resolve ModuleNotFoundError for backend, add E2E test infra
- Dockerfile.agent: fix CMD (python -m src.agent.run), use backend/requirements.txt,
  minimal COPY (only src.agent + src.core.cot_logger), add GRACE contract
- docker-compose.yml: SERVICE_TOKEN_SECRET -> SERVICE_JWT (match code)
- docker-compose.enterprise-clean.yml: same env var fix
- docker/.env.agent.example: same env var fix
- build.sh: same env var fix
- chore: semantics-testing SKILL.md, backend tests, pyproject.toml
2026-06-14 15:41:46 +03:00

348 lines
14 KiB
Python

# #region Test.Git.Url [C:3] [TYPE Module] [SEMANTICS test,git,url,parse,remote]
# @BRIEF Tests for GitServiceUrlMixin — host extraction, credential stripping, host replacement, origin alignment, remote identity parsing, server URL derivation, URL normalization.
# @RELATION BINDS_TO -> [GitServiceUrlMixin]
# @TEST_EDGE: empty_url -> returns None or empty
# @TEST_EDGE: non_http_scheme -> returns None (ssh/git protocol)
# @TEST_EDGE: url_with_port -> host:port preserved
# @TEST_EDGE: url_with_credentials -> credentials stripped/replaced correctly
# @TEST_EDGE: ssh_url -> parsed via git@ split
# @TEST_EDGE: missing_owner_repo -> raises HTTPException 400
# @TEST_EDGE: empty_server_url -> raises HTTPException 400
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
import pytest
from unittest.mock import MagicMock, patch
from fastapi import HTTPException
from src.services.git._url import GitServiceUrlMixin
class TestableGitUrl(GitServiceUrlMixin):
"""Minimal concrete subclass for testing URL mixin methods."""
pass
# ── _extract_http_host ──
class TestExtractHttpHost:
"""_extract_http_host — host[:port] extraction from HTTP(S) URLs."""
# #region test_extract_host_https [C:2] [TYPE Function]
def test_extract_host_https(self):
"""Standard HTTPS URL returns lowercase host."""
svc = TestableGitUrl()
result = svc._extract_http_host("https://Git.Example.COM/org/repo.git")
assert result == "git.example.com"
# #endregion test_extract_host_https
# #region test_extract_host_with_port [C:2] [TYPE Function]
def test_extract_host_with_port(self):
"""URL with port returns host:port."""
svc = TestableGitUrl()
result = svc._extract_http_host("http://gitea.local:3000/repo")
assert result == "gitea.local:3000"
# #endregion test_extract_host_with_port
# #region test_extract_host_empty [C:2] [TYPE Function]
def test_extract_host_empty(self):
"""Empty/None input returns None."""
svc = TestableGitUrl()
assert svc._extract_http_host(None) is None
assert svc._extract_http_host("") is None
assert svc._extract_http_host(" ") is None
# #endregion test_extract_host_empty
# #region test_extract_host_non_http [C:2] [TYPE Function]
def test_extract_host_non_http(self):
"""SSH or FTP scheme returns None."""
svc = TestableGitUrl()
assert svc._extract_http_host("git@github.com:org/repo.git") is None
assert svc._extract_http_host("ftp://files.example.com/repo") is None
# #endregion test_extract_host_non_http
# #region test_extract_host_no_hostname [C:2] [TYPE Function]
def test_extract_host_no_hostname(self):
"""Malformed URL with no hostname returns None."""
svc = TestableGitUrl()
assert svc._extract_http_host("http://") is None
# #endregion test_extract_host_no_hostname
# ── _strip_url_credentials ──
class TestStripUrlCredentials:
"""_strip_url_credentials — remove user:pass from URL."""
# #region test_strip_credentials [C:2] [TYPE Function]
def test_strip_credentials(self):
"""Credentials removed, scheme/host/path preserved."""
svc = TestableGitUrl()
result = svc._strip_url_credentials("https://oauth2:token123@git.example.com/org/repo.git")
assert result == "https://git.example.com/org/repo.git"
# #endregion test_strip_credentials
# #region test_strip_no_credentials [C:2] [TYPE Function]
def test_strip_no_credentials(self):
"""URL without credentials returned unchanged."""
svc = TestableGitUrl()
result = svc._strip_url_credentials("https://git.example.com/org/repo.git")
assert result == "https://git.example.com/org/repo.git"
# #endregion test_strip_no_credentials
# #region test_strip_empty [C:2] [TYPE Function]
def test_strip_empty(self):
"""Empty string returned as-is."""
svc = TestableGitUrl()
assert svc._strip_url_credentials("") == ""
# #endregion test_strip_empty
# #region test_strip_non_http_passthrough [C:2] [TYPE Function]
def test_strip_non_http_passthrough(self):
"""Non-HTTP URL returned unchanged."""
svc = TestableGitUrl()
url = "git@github.com:org/repo.git"
assert svc._strip_url_credentials(url) == url
# #endregion test_strip_non_http_passthrough
# #region test_strip_with_port [C:2] [TYPE Function]
def test_strip_with_port(self):
"""Port preserved after credential stripping."""
svc = TestableGitUrl()
result = svc._strip_url_credentials("https://user:pass@gitea.local:3000/repo")
assert result == "https://gitea.local:3000/repo"
# #endregion test_strip_with_port
# ── _replace_host_in_url ──
class TestReplaceHostInUrl:
"""_replace_host_in_url — swap origin host with config host."""
# #region test_replace_host_basic [C:2] [TYPE Function]
def test_replace_host_basic(self):
"""Source host replaced with config host, credentials preserved."""
svc = TestableGitUrl()
result = svc._replace_host_in_url(
"https://old-host.com/org/repo.git",
"https://new-host.com",
)
assert result == "https://new-host.com/org/repo.git"
# #endregion test_replace_host_basic
# #region test_replace_host_preserves_auth [C:2] [TYPE Function]
def test_replace_host_preserves_auth(self):
"""Credentials from source URL preserved on new host."""
svc = TestableGitUrl()
result = svc._replace_host_in_url(
"https://oauth2:tok123@old-host.com/org/repo.git",
"https://new-host.com:3000",
)
assert "oauth2:tok123@new-host.com:3000" in result
assert "/org/repo.git" in result
# #endregion test_replace_host_preserves_auth
# #region test_replace_host_empty_inputs [C:2] [TYPE Function]
def test_replace_host_empty_inputs(self):
"""Empty source or config returns None."""
svc = TestableGitUrl()
assert svc._replace_host_in_url(None, "https://x.com") is None
assert svc._replace_host_in_url("https://x.com", None) is None
assert svc._replace_host_in_url("", "") is None
# #endregion test_replace_host_empty_inputs
# #region test_replace_host_non_http [C:2] [TYPE Function]
def test_replace_host_non_http(self):
"""Non-HTTP source or config returns None."""
svc = TestableGitUrl()
assert svc._replace_host_in_url("git@old:x.git", "https://new.com") is None
assert svc._replace_host_in_url("https://old.com/x", "ftp://new.com") is None
# #endregion test_replace_host_non_http
# ── _parse_remote_repo_identity ──
class TestParseRemoteRepoIdentity:
"""_parse_remote_repo_identity — extract owner/repo from remote URL."""
# #region test_parse_https_url [C:2] [TYPE Function]
def test_parse_https_url(self):
"""HTTPS URL parsed into owner, repo, namespace."""
svc = TestableGitUrl()
result = svc._parse_remote_repo_identity("https://gitea.example.com/org/repo.git")
assert result["owner"] == "org"
assert result["repo"] == "repo"
assert result["namespace"] == "org"
assert result["full_name"] == "org/repo"
# #endregion test_parse_https_url
# #region test_parse_ssh_url [C:2] [TYPE Function]
def test_parse_ssh_url(self):
"""SSH git@ URL parsed via colon split."""
svc = TestableGitUrl()
result = svc._parse_remote_repo_identity("git@github.com:myorg/myrepo.git")
assert result["owner"] == "myorg"
assert result["repo"] == "myrepo"
assert result["namespace"] == "myorg"
# #endregion test_parse_ssh_url
# #region test_parse_nested_namespace [C:2] [TYPE Function]
def test_parse_nested_namespace(self):
"""Nested namespace (subgroups) preserved."""
svc = TestableGitUrl()
result = svc._parse_remote_repo_identity("https://gitlab.com/group/subgroup/project.git")
assert result["owner"] == "group"
assert result["repo"] == "project"
assert result["namespace"] == "group/subgroup"
assert result["full_name"] == "group/subgroup/project"
# #endregion test_parse_nested_namespace
# #region test_parse_empty_url_raises [C:2] [TYPE Function]
def test_parse_empty_url_raises(self):
"""Empty URL raises HTTPException 400."""
svc = TestableGitUrl()
with pytest.raises(HTTPException, match="empty"):
svc._parse_remote_repo_identity("")
# #endregion test_parse_empty_url_raises
# #region test_parse_single_segment_raises [C:2] [TYPE Function]
def test_parse_single_segment_raises(self):
"""URL with <2 path segments raises HTTPException 400."""
svc = TestableGitUrl()
with pytest.raises(HTTPException, match="Cannot parse"):
svc._parse_remote_repo_identity("https://host.com/onlyone")
# #endregion test_parse_single_segment_raises
# #region test_parse_no_git_suffix [C:2] [TYPE Function]
def test_parse_no_git_suffix(self):
"""URL without .git suffix parsed correctly."""
svc = TestableGitUrl()
result = svc._parse_remote_repo_identity("https://host.com/owner/repo")
assert result["repo"] == "repo"
# #endregion test_parse_no_git_suffix
# ── _derive_server_url_from_remote ──
class TestDeriveServerUrlFromRemote:
"""_derive_server_url_from_remote — build API base URL."""
# #region test_derive_https [C:2] [TYPE Function]
def test_derive_https(self):
"""HTTPS URL → scheme://host base."""
svc = TestableGitUrl()
result = svc._derive_server_url_from_remote("https://gitea.example.com/org/repo.git")
assert result == "https://gitea.example.com"
# #endregion test_derive_https
# #region test_derive_with_port [C:2] [TYPE Function]
def test_derive_with_port(self):
"""Port preserved in derived URL."""
svc = TestableGitUrl()
result = svc._derive_server_url_from_remote("http://gitea.local:3000/repo")
assert result == "http://gitea.local:3000"
# #endregion test_derive_with_port
# #region test_derive_ssh_returns_none [C:2] [TYPE Function]
def test_derive_ssh_returns_none(self):
"""SSH URL returns None."""
svc = TestableGitUrl()
assert svc._derive_server_url_from_remote("git@host:org/repo.git") is None
# #endregion test_derive_ssh_returns_none
# #region test_derive_empty_returns_none [C:2] [TYPE Function]
def test_derive_empty_returns_none(self):
"""Empty input returns None."""
svc = TestableGitUrl()
assert svc._derive_server_url_from_remote("") is None
assert svc._derive_server_url_from_remote(None) is None
# #endregion test_derive_empty_returns_none
# ── _normalize_git_server_url ──
class TestNormalizeGitServerUrl:
"""_normalize_git_server_url — strip trailing slash."""
# #region test_normalize_strips_slash [C:2] [TYPE Function]
def test_normalize_strips_slash(self):
"""Trailing slash removed."""
svc = TestableGitUrl()
assert svc._normalize_git_server_url("https://gitea.com/") == "https://gitea.com"
# #endregion test_normalize_strips_slash
# #region test_normalize_no_change [C:2] [TYPE Function]
def test_normalize_no_change(self):
"""URL without trailing slash unchanged."""
svc = TestableGitUrl()
assert svc._normalize_git_server_url("https://gitea.com") == "https://gitea.com"
# #endregion test_normalize_no_change
# #region test_normalize_empty_raises [C:2] [TYPE Function]
def test_normalize_empty_raises(self):
"""Empty URL raises HTTPException 400."""
svc = TestableGitUrl()
with pytest.raises(HTTPException, match="required"):
svc._normalize_git_server_url("")
# #endregion test_normalize_empty_raises
# ── _align_origin_host_with_config ──
class TestAlignOriginHostWithConfig:
"""_align_origin_host_with_config — auto-align origin host drift."""
# #region test_align_no_drift [C:2] [TYPE Function]
def test_align_no_drift(self):
"""Matching hosts → no action, returns None."""
svc = TestableGitUrl()
origin = MagicMock()
result = svc._align_origin_host_with_config(
dashboard_id=1,
origin=origin,
config_url="https://gitea.com",
current_origin_url="https://gitea.com/org/repo.git",
binding_remote_url=None,
)
assert result is None
origin.set_url.assert_not_called()
# #endregion test_align_no_drift
# #region test_align_host_mismatch [C:2] [TYPE Function]
@patch("src.services.git._url.SessionLocal")
def test_align_host_mismatch(self, mock_session_cls):
"""Mismatched hosts → origin.set_url called, DB updated."""
mock_session = MagicMock()
mock_session_cls.return_value = mock_session
mock_session.query.return_value.filter.return_value.first.return_value = None
svc = TestableGitUrl()
origin = MagicMock()
result = svc._align_origin_host_with_config(
dashboard_id=1,
origin=origin,
config_url="https://new-host.com",
current_origin_url="https://old-host.com/org/repo.git",
binding_remote_url=None,
)
assert result is not None
assert "new-host.com" in result
origin.set_url.assert_called_once()
# #endregion test_align_host_mismatch
# #region test_align_missing_config [C:2] [TYPE Function]
def test_align_missing_config(self):
"""No config URL → returns None, no action."""
svc = TestableGitUrl()
origin = MagicMock()
result = svc._align_origin_host_with_config(
dashboard_id=1, origin=origin, config_url=None,
current_origin_url="https://host.com/repo", binding_remote_url=None,
)
assert result is None
# #endregion test_align_missing_config
# #endregion Test.Git.Url