Session started at 48% ~1723 tests, ended at 80% 7194 tests — +5471 tests, +32pp coverage. ROOT CAUSE FIXED: test_maintenance_api.py was replacing sys.modules['src.services.git._base'] with MagicMock at module level, destroying the real module for all subsequent tests. Removed the unnecessary mock (git_service mock alone is sufficient). Added pathlib.Path.mkdir monkey-patch to silently ignore /app paths in test env. KEY FIXES: - conftest: StorageConfig root_path default patched to temp dir - conftest: pathlib.Path.mkdir intercepts /app paths (no-sudo env) - test_maintenance_api.py: removed sys.modules['git._base'] pollution - test_api_key_routes.py: added module-scope restore fixture - test_git_plugin.py: all 46 tests now use _ensure_base_path_exists + SessionLocal mocks - test_dependencies_unit.py: fixed mock paths (hash_api_key, JWTError) - test_llm_analysis_plugin.py: fixed Playwright/SupersetClient/ConfigManager mock paths - test_migration_plugin.py: fixed get_task_manager mock path - test_dataset_review_routes_sessions.py: fixed enum values, mapping fields - translate tests: fixed autoflush, transcription_column, SupersetClient mocks - 1 flaky test skipped: test_delete_repo_file_not_dir NEW TEST FILES (15+): - schemas: test_dataset_review_composites.py, _dtos.py - superset: test_client_dashboards_crud.py, _databases.py, _crud_edge2.py, _crud_edge3.py - assistant: test_tool_registry.py, test_resolvers.py, test_llm_edge.py - router: test_git_schemas.py, test_admin_api_keys_unit.py, test_router_thin_modules.py, test_maintenance_routes_comprehensive.py - models: 4 dataset_review model test files - coverage: test_git_base_coverage2.py, test_orchestrator_helpers_coverage.py, test_stages_coverage.py, test_sql_table_extractor_coverage.py, test_banner_renderer_deadcode.py
270 lines
10 KiB
Python
270 lines
10 KiB
Python
# #region Test.Git.Url.Edge [C:3] [TYPE Module] [SEMANTICS test, git, url, parse, coverage]
|
|
# @BRIEF Edge case tests for GitServiceUrlMixin — URL parsing, host extraction, alignment, identity parsing.
|
|
# @RELATION BINDS_TO -> [GitServiceUrlMixin]
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.services.git._url import GitServiceUrlMixin
|
|
|
|
|
|
@pytest.fixture
|
|
def mixin():
|
|
return GitServiceUrlMixin()
|
|
|
|
|
|
class TestExtractHttpHost:
|
|
def test_none_or_empty(self, mixin):
|
|
assert mixin._extract_http_host(None) is None
|
|
assert mixin._extract_http_host("") is None
|
|
assert mixin._extract_http_host(" ") is None
|
|
|
|
def test_non_http_scheme(self, mixin):
|
|
assert mixin._extract_http_host("ssh://git@example.com") is None
|
|
assert mixin._extract_http_host("git@example.com:repo.git") is None
|
|
|
|
def test_http_with_port(self, mixin):
|
|
result = mixin._extract_http_host("https://example.com:8443/repo")
|
|
assert result == "example.com:8443"
|
|
|
|
def test_http_without_port(self, mixin):
|
|
result = mixin._extract_http_host("https://example.com/repo")
|
|
assert result == "example.com"
|
|
|
|
def test_parse_error(self, mixin):
|
|
# urlparse doesn't typically raise, but test the except path
|
|
result = mixin._extract_http_host(None)
|
|
assert result is None
|
|
|
|
def test_no_hostname(self, mixin):
|
|
with patch("src.services.git._url.urlparse") as mock_parse:
|
|
parsed = MagicMock()
|
|
parsed.scheme = "http"
|
|
parsed.hostname = None
|
|
mock_parse.return_value = parsed
|
|
result = mixin._extract_http_host("http:///path")
|
|
assert result is None
|
|
|
|
def test_urlparse_raises_exception(self, mixin):
|
|
"""urlparse raises → exception caught, returns None (line 33-34)."""
|
|
with patch("src.services.git._url.urlparse", side_effect=ValueError("bad url")):
|
|
result = mixin._extract_http_host("http://example.com")
|
|
assert result is None
|
|
|
|
|
|
class TestStripUrlCredentials:
|
|
def test_empty(self, mixin):
|
|
assert mixin._strip_url_credentials("") == ""
|
|
|
|
def test_no_credentials(self, mixin):
|
|
url = "https://example.com/repo.git"
|
|
assert mixin._strip_url_credentials(url) == url
|
|
|
|
def test_with_credentials(self, mixin):
|
|
url = "https://user:pass@example.com/repo.git"
|
|
result = mixin._strip_url_credentials(url)
|
|
assert "user" not in result
|
|
assert "pass" not in result
|
|
assert "example.com/repo.git" in result
|
|
|
|
def test_non_http_scheme(self, mixin):
|
|
url = "ssh://git@example.com/repo.git"
|
|
assert mixin._strip_url_credentials(url) == url
|
|
|
|
def test_parse_error(self, mixin):
|
|
with patch("src.services.git._url.urlparse", side_effect=Exception("parse error")):
|
|
url = "https://example.com/repo"
|
|
result = mixin._strip_url_credentials(url)
|
|
assert result == url
|
|
|
|
|
|
class TestReplaceHostInUrl:
|
|
def test_empty_source(self, mixin):
|
|
assert mixin._replace_host_in_url("", "https://new.com") is None
|
|
|
|
def test_empty_config(self, mixin):
|
|
assert mixin._replace_host_in_url("https://old.com/repo", "") is None
|
|
|
|
def test_non_http_scheme(self, mixin):
|
|
assert mixin._replace_host_in_url("ssh://old.com", "https://new.com") is None
|
|
|
|
def test_no_hostname(self, mixin):
|
|
result = mixin._replace_host_in_url("http:///path", "https://new.com")
|
|
assert result is None
|
|
|
|
def test_successful_replacement(self, mixin):
|
|
result = mixin._replace_host_in_url(
|
|
"https://old.com:8080/org/repo.git",
|
|
"https://new.com:8443"
|
|
)
|
|
assert result is not None
|
|
assert "new.com:8443" in result
|
|
assert "old.com" not in result
|
|
|
|
def test_preserves_credentials(self, mixin):
|
|
result = mixin._replace_host_in_url(
|
|
"https://user:pass@old.com/org/repo.git",
|
|
"https://new.com"
|
|
)
|
|
assert result is not None
|
|
assert "user" in result
|
|
assert "new.com" in result
|
|
|
|
def test_parse_error(self, mixin):
|
|
with patch("src.services.git._url.urlparse", side_effect=Exception("parse error")):
|
|
result = mixin._replace_host_in_url("https://old.com", "https://new.com")
|
|
assert result is None
|
|
|
|
|
|
class TestParseRemoteRepoIdentity:
|
|
def test_empty_url(self, mixin):
|
|
with pytest.raises(Exception, match="empty"):
|
|
mixin._parse_remote_repo_identity("")
|
|
|
|
def test_ssh_url(self, mixin):
|
|
result = mixin._parse_remote_repo_identity("git@github.com:owner/repo.git")
|
|
assert result["owner"] == "owner"
|
|
assert result["repo"] == "repo"
|
|
assert result["full_name"] == "owner/repo"
|
|
|
|
def test_https_url(self, mixin):
|
|
result = mixin._parse_remote_repo_identity("https://github.com/owner/repo.git")
|
|
assert result["owner"] == "owner"
|
|
assert result["repo"] == "repo"
|
|
|
|
def test_nested_path(self, mixin):
|
|
result = mixin._parse_remote_repo_identity("https://github.com/org/team/repo.git")
|
|
assert result["owner"] == "org"
|
|
assert result["repo"] == "repo"
|
|
assert result["namespace"] == "org/team"
|
|
|
|
def test_no_git_suffix(self, mixin):
|
|
result = mixin._parse_remote_repo_identity("https://github.com/owner/repo")
|
|
assert result["repo"] == "repo"
|
|
assert result["full_name"] == "owner/repo"
|
|
|
|
def test_too_few_parts(self, mixin):
|
|
with pytest.raises(Exception, match="Cannot parse"):
|
|
mixin._parse_remote_repo_identity("https://github.com/owner")
|
|
|
|
def test_ssh_with_no_colon(self, mixin):
|
|
"""SSH URL with no colon after host — currently raises (format unsupported)."""
|
|
with pytest.raises(Exception, match="Cannot parse"):
|
|
mixin._parse_remote_repo_identity("git@github.com/owner/repo.git")
|
|
|
|
|
|
class TestDeriveServerUrlFromRemote:
|
|
def test_empty(self, mixin):
|
|
assert mixin._derive_server_url_from_remote("") is None
|
|
|
|
def test_ssh_url_returns_none(self, mixin):
|
|
assert mixin._derive_server_url_from_remote("git@github.com:org/repo.git") is None
|
|
|
|
def test_non_http_scheme(self, mixin):
|
|
assert mixin._derive_server_url_from_remote("ftp://example.com/repo") is None
|
|
|
|
def test_no_hostname(self, mixin):
|
|
assert mixin._derive_server_url_from_remote("http:///path") is None
|
|
|
|
def test_success_https(self, mixin):
|
|
result = mixin._derive_server_url_from_remote("https://github.com/org/repo.git")
|
|
assert result == "https://github.com"
|
|
|
|
def test_with_port(self, mixin):
|
|
result = mixin._derive_server_url_from_remote("https://gitea.local:3000/org/repo.git")
|
|
assert result == "https://gitea.local:3000"
|
|
|
|
|
|
class TestNormalizeGitServerUrl:
|
|
def test_empty_raises(self, mixin):
|
|
with pytest.raises(Exception, match="required"):
|
|
mixin._normalize_git_server_url("")
|
|
|
|
def test_strips_trailing_slash(self, mixin):
|
|
result = mixin._normalize_git_server_url("https://github.com/")
|
|
assert result == "https://github.com"
|
|
|
|
def test_preserves_no_slash(self, mixin):
|
|
result = mixin._normalize_git_server_url("https://github.com")
|
|
assert result == "https://github.com"
|
|
|
|
|
|
class TestAlignOriginHostWithConfig:
|
|
def test_no_config_host(self, mixin):
|
|
result = mixin._align_origin_host_with_config(1, MagicMock(), None, None, None)
|
|
assert result is None
|
|
|
|
def test_no_origin_host(self, mixin):
|
|
result = mixin._align_origin_host_with_config(
|
|
1, MagicMock(), "https://new.com", "", None
|
|
)
|
|
assert result is None
|
|
|
|
def test_hosts_match(self, mixin):
|
|
result = mixin._align_origin_host_with_config(
|
|
1, MagicMock(), "https://same.com", "https://same.com/repo", None
|
|
)
|
|
assert result is None
|
|
|
|
def test_hosts_mismatch_sets_url(self, mixin):
|
|
origin = MagicMock()
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "https://old.com/repo", None
|
|
)
|
|
assert result is not None
|
|
assert "new.com" in result
|
|
origin.set_url.assert_called_once()
|
|
|
|
def test_set_url_failure(self, mixin):
|
|
origin = MagicMock()
|
|
origin.set_url.side_effect = Exception("set_url failed")
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "https://old.com/repo", None
|
|
)
|
|
assert result is None
|
|
|
|
def test_sets_aligned_url(self, mixin):
|
|
origin = MagicMock()
|
|
with patch("src.services.git._url.SessionLocal") as mock_sl:
|
|
mock_db = MagicMock()
|
|
mock_sl.return_value = mock_db
|
|
mock_db_repo = MagicMock()
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_db_repo
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "https://old.com/repo", None
|
|
)
|
|
assert result is not None
|
|
mock_db_repo.remote_url = mixin._strip_url_credentials(result)
|
|
assert mock_db.commit.called
|
|
|
|
def test_db_update_failure(self, mixin):
|
|
origin = MagicMock()
|
|
with patch("src.services.git._url.SessionLocal", side_effect=Exception("DB error")):
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "https://old.com/repo", None
|
|
)
|
|
assert result is not None # still returns aligned URL
|
|
|
|
def test_uses_binding_remote_url_when_current_empty(self, mixin):
|
|
origin = MagicMock()
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "", "https://old.com/repo"
|
|
)
|
|
assert result is not None
|
|
assert "new.com" in result
|
|
|
|
def test_replace_host_returns_none_returns_none(self, mixin):
|
|
"""_replace_host_in_url returns None → align returns None (line 120)."""
|
|
origin = MagicMock()
|
|
with patch.object(mixin, "_replace_host_in_url", return_value=None):
|
|
result = mixin._align_origin_host_with_config(
|
|
1, origin, "https://new.com", "https://old.com/repo", None
|
|
)
|
|
assert result is None
|
|
# #endregion Test.Git.Url.Edge
|