# #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