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
348 lines
14 KiB
Python
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
|