Files
ss-tools/backend/tests/services/git/test_git_url.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +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 -> [Services.Url.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.Git.TestExtractHostHttps [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.Git.TestExtractHostHttps
# #region Test.Git.TestExtractHostWithPort [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.Git.TestExtractHostWithPort
# #region Test.Git.TestExtractHostEmpty [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.Git.TestExtractHostEmpty
# #region Test.Git.TestExtractHostNonHttp [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.Git.TestExtractHostNonHttp
# #region Test.Git.TestExtractHostNoHostname [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.Git.TestExtractHostNoHostname
# ── _strip_url_credentials ──
class TestStripUrlCredentials:
"""_strip_url_credentials — remove user:pass from URL."""
# #region Test.Git.TestStripCredentials [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.Git.TestStripCredentials
# #region Test.Git.TestStripNoCredentials [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.Git.TestStripNoCredentials
# #region Test.Git.TestStripEmpty [C:2] [TYPE Function]
def test_strip_empty(self):
"""Empty string returned as-is."""
svc = TestableGitUrl()
assert svc._strip_url_credentials("") == ""
# #endregion Test.Git.TestStripEmpty
# #region Test.Git.TestStripNonHttpPassthrough [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.Git.TestStripNonHttpPassthrough
# #region Test.Git.TestStripWithPort [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.Git.TestStripWithPort
# ── _replace_host_in_url ──
class TestReplaceHostInUrl:
"""_replace_host_in_url — swap origin host with config host."""
# #region Test.Git.TestReplaceHostBasic [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.Git.TestReplaceHostBasic
# #region Test.Git.TestReplaceHostPreservesAuth [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.Git.TestReplaceHostPreservesAuth
# #region Test.Git.TestReplaceHostEmptyInputs [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.Git.TestReplaceHostEmptyInputs
# #region Test.Git.TestReplaceHostNonHttp [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.Git.TestReplaceHostNonHttp
# ── _parse_remote_repo_identity ──
class TestParseRemoteRepoIdentity:
"""_parse_remote_repo_identity — extract owner/repo from remote URL."""
# #region Test.Git.TestParseHttpsUrl [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.Git.TestParseHttpsUrl
# #region Test.Git.TestParseSshUrl [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.Git.TestParseSshUrl
# #region Test.Git.TestParseNestedNamespace [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.Git.TestParseNestedNamespace
# #region Test.Git.TestParseEmptyUrlRaises [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.Git.TestParseEmptyUrlRaises
# #region Test.Git.TestParseSingleSegmentRaises [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.Git.TestParseSingleSegmentRaises
# #region Test.Git.TestParseNoGitSuffix [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.Git.TestParseNoGitSuffix
# ── _derive_server_url_from_remote ──
class TestDeriveServerUrlFromRemote:
"""_derive_server_url_from_remote — build API base URL."""
# #region Test.Git.TestDeriveHttps [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.Git.TestDeriveHttps
# #region Test.Git.TestDeriveWithPort [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.Git.TestDeriveWithPort
# #region Test.Git.TestDeriveSshReturnsNone [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.Git.TestDeriveSshReturnsNone
# #region Test.Git.TestDeriveEmptyReturnsNone [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.Git.TestDeriveEmptyReturnsNone
# ── _normalize_git_server_url ──
class TestNormalizeGitServerUrl:
"""_normalize_git_server_url — strip trailing slash."""
# #region Test.Git.TestNormalizeStripsSlash [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.Git.TestNormalizeStripsSlash
# #region Test.Git.TestNormalizeNoChange [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.Git.TestNormalizeNoChange
# #region Test.Git.TestNormalizeEmptyRaises [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.Git.TestNormalizeEmptyRaises
# ── _align_origin_host_with_config ──
class TestAlignOriginHostWithConfig:
"""_align_origin_host_with_config — auto-align origin host drift."""
# #region Test.Git.TestAlignNoDrift [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.Git.TestAlignNoDrift
# #region Test.Git.TestAlignHostMismatch [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.Git.TestAlignHostMismatch
# #region Test.Git.TestAlignMissingConfig [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.Git.TestAlignMissingConfig
# #endregion Test.Git.Url