fix: commit verified semantic repair changes

This commit is contained in:
2026-03-21 11:05:20 +03:00
parent 5cca35f8d5
commit 1ce61d9533
10 changed files with 2504 additions and 901 deletions

View File

@@ -1,5 +1,6 @@
# [DEF:backend.src.api.routes.__tests__.test_git_api:Module]
# @RELATION: VERIFIES -> src.api.routes.git
# [DEF:TestGitApi:Module]
# @COMPLEXITY: 3
# @RELATION: VERIFIES ->[src.api.routes.git]
# @PURPOSE: API tests for Git configurations and repository operations.
import pytest
@@ -9,32 +10,52 @@ from fastapi import HTTPException
from src.api.routes import git as git_routes
from src.models.git import GitServerConfig, GitProvider, GitStatus, GitRepository
class DbMock:
def __init__(self, data=None):
self._data = data or []
self._deleted = []
self._added = []
self._filtered = None
def query(self, model):
self._model = model
self._filtered = None
return self
def filter(self, condition):
# Simplistic mocking for tests, assuming equality checks
for item in self._data:
# We assume condition is an equality expression like GitServerConfig.id == "123"
# It's hard to eval the condition exactly in a mock without complex parsing,
# so we'll just return items where type matches.
pass
# Honor simple SQLAlchemy equality expressions used by these route tests.
candidates = [
item
for item in self._data
if not hasattr(self, "_model") or isinstance(item, self._model)
]
try:
left_key = getattr(getattr(condition, "left", None), "key", None)
right_value = getattr(getattr(condition, "right", None), "value", None)
if left_key is not None and right_value is not None:
self._filtered = [
item
for item in candidates
if getattr(item, left_key, None) == right_value
]
else:
self._filtered = candidates
except Exception:
self._filtered = candidates
return self
def first(self):
if self._filtered is not None:
return self._filtered[0] if self._filtered else None
for item in self._data:
if hasattr(self, "_model") and isinstance(item, self._model):
return item
return None
def all(self):
if self._filtered is not None:
return list(self._filtered)
return self._data
def add(self, item):
@@ -57,254 +78,410 @@ class DbMock:
if not hasattr(item, "last_validated"):
item.last_validated = "2026-03-08T00:00:00Z"
# [DEF:test_get_git_configs_masks_pat:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_get_git_configs_masks_pat():
"""
@PRE: Database session `db` is available.
@POST: Returns a list of all GitServerConfig objects from the database with PAT masked.
"""
db = DbMock([GitServerConfig(
id="config-1", name="Test Server", provider=GitProvider.GITHUB,
url="https://github.com", pat="secret-token",
status=GitStatus.CONNECTED, last_validated="2026-03-08T00:00:00Z"
)])
db = DbMock(
[
GitServerConfig(
id="config-1",
name="Test Server",
provider=GitProvider.GITHUB,
url="https://github.com",
pat="secret-token",
status=GitStatus.CONNECTED,
last_validated="2026-03-08T00:00:00Z",
)
]
)
result = asyncio.run(git_routes.get_git_configs(db=db))
assert len(result) == 1
assert result[0].pat == "********"
assert result[0].name == "Test Server"
# [/DEF:test_get_git_configs_masks_pat:Function]
# [DEF:test_create_git_config_persists_config:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_create_git_config_persists_config():
"""
@PRE: `config` contains valid GitServerConfigCreate data.
@POST: A new GitServerConfig record is created in the database.
"""
from src.api.routes.git_schemas import GitServerConfigCreate
db = DbMock()
config = GitServerConfigCreate(
name="New Server", provider=GitProvider.GITLAB,
url="https://gitlab.com", pat="new-token",
default_branch="master"
name="New Server",
provider=GitProvider.GITLAB,
url="https://gitlab.com",
pat="new-token",
default_branch="master",
)
result = asyncio.run(git_routes.create_git_config(config=config, db=db))
assert len(db._added) == 1
assert db._added[0].name == "New Server"
assert db._added[0].pat == "new-token"
assert result.name == "New Server"
assert result.pat == "new-token" # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.
assert (
result.pat == "new-token"
) # Note: route returns unmasked until serialized by FastAPI usually, but in tests schema might catch it or not.
# [/DEF:test_create_git_config_persists_config:Function]
from src.api.routes.git_schemas import GitServerConfigUpdate
# [DEF:test_update_git_config_modifies_record:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_update_git_config_modifies_record():
"""
@PRE: `config_id` corresponds to an existing configuration.
@POST: The configuration record is updated in the database, preserving PAT if masked is sent.
"""
existing_config = GitServerConfig(
id="config-1", name="Old Server", provider=GitProvider.GITHUB,
url="https://github.com", pat="old-token",
status=GitStatus.CONNECTED, last_validated="2026-03-08T00:00:00Z"
id="config-1",
name="Old Server",
provider=GitProvider.GITHUB,
url="https://github.com",
pat="old-token",
status=GitStatus.CONNECTED,
last_validated="2026-03-08T00:00:00Z",
)
# The monkeypatched query will return existing_config as it's the only one in the list
class SingleConfigDbMock:
def query(self, *args): return self
def filter(self, *args): return self
def first(self): return existing_config
def commit(self): pass
def refresh(self, config): pass
def query(self, *args):
return self
def filter(self, *args):
return self
def first(self):
return existing_config
def commit(self):
pass
def refresh(self, config):
pass
db = SingleConfigDbMock()
update_data = GitServerConfigUpdate(name="Updated Server", pat="********")
result = asyncio.run(git_routes.update_git_config(config_id="config-1", config_update=update_data, db=db))
result = asyncio.run(
git_routes.update_git_config(
config_id="config-1", config_update=update_data, db=db
)
)
assert existing_config.name == "Updated Server"
assert existing_config.pat == "old-token" # Ensure PAT is not overwritten with asterisks
assert (
existing_config.pat == "old-token"
) # Ensure PAT is not overwritten with asterisks
assert result.pat == "********"
# [/DEF:test_update_git_config_modifies_record:Function]
# [DEF:test_update_git_config_raises_404_if_not_found:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_update_git_config_raises_404_if_not_found():
"""
@PRE: `config_id` corresponds to a missing configuration.
@THROW: HTTPException 404
"""
db = DbMock([]) # Empty db
db = DbMock([]) # Empty db
update_data = GitServerConfigUpdate(name="Updated Server", pat="new-token")
with pytest.raises(HTTPException) as exc_info:
asyncio.run(git_routes.update_git_config(config_id="config-1", config_update=update_data, db=db))
asyncio.run(
git_routes.update_git_config(
config_id="config-1", config_update=update_data, db=db
)
)
assert exc_info.value.status_code == 404
assert exc_info.value.detail == "Configuration not found"
# [/DEF:test_update_git_config_raises_404_if_not_found:Function]
# [DEF:test_delete_git_config_removes_record:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_delete_git_config_removes_record():
"""
@PRE: `config_id` corresponds to an existing configuration.
@POST: The configuration record is removed from the database.
"""
existing_config = GitServerConfig(id="config-1")
class SingleConfigDbMock:
def query(self, *args): return self
def filter(self, *args): return self
def first(self): return existing_config
def delete(self, config): self.deleted = config
def commit(self): pass
def query(self, *args):
return self
def filter(self, *args):
return self
def first(self):
return existing_config
def delete(self, config):
self.deleted = config
def commit(self):
pass
db = SingleConfigDbMock()
result = asyncio.run(git_routes.delete_git_config(config_id="config-1", db=db))
assert db.deleted == existing_config
assert result["status"] == "success"
# [/DEF:test_delete_git_config_removes_record:Function]
# [DEF:test_test_git_config_validates_connection_successfully:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_test_git_config_validates_connection_successfully(monkeypatch):
"""
@PRE: `config` contains provider, url, and pat.
@POST: Returns success if the connection is validated via GitService.
"""
class MockGitService:
async def test_connection(self, provider, url, pat):
return True
monkeypatch.setattr(git_routes, "git_service", MockGitService())
from src.api.routes.git_schemas import GitServerConfigCreate
config = GitServerConfigCreate(
name="Test Server", provider=GitProvider.GITHUB,
url="https://github.com", pat="test-pat"
name="Test Server",
provider=GitProvider.GITHUB,
url="https://github.com",
pat="test-pat",
)
db = DbMock([])
result = asyncio.run(git_routes.test_git_config(config=config, db=db))
assert result["status"] == "success"
# [/DEF:test_test_git_config_validates_connection_successfully:Function]
# [DEF:test_test_git_config_fails_validation:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_test_git_config_fails_validation(monkeypatch):
"""
@PRE: `config` contains provider, url, and pat BUT connection fails.
@THROW: HTTPException 400
"""
class MockGitService:
async def test_connection(self, provider, url, pat):
return False
monkeypatch.setattr(git_routes, "git_service", MockGitService())
from src.api.routes.git_schemas import GitServerConfigCreate
config = GitServerConfigCreate(
name="Test Server", provider=GitProvider.GITHUB,
url="https://github.com", pat="bad-pat"
name="Test Server",
provider=GitProvider.GITHUB,
url="https://github.com",
pat="bad-pat",
)
db = DbMock([])
with pytest.raises(HTTPException) as exc_info:
asyncio.run(git_routes.test_git_config(config=config, db=db))
assert exc_info.value.status_code == 400
assert exc_info.value.detail == "Connection failed"
# [/DEF:test_test_git_config_fails_validation:Function]
# [DEF:test_list_gitea_repositories_returns_payload:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_list_gitea_repositories_returns_payload(monkeypatch):
"""
@PRE: config_id exists and provider is GITEA.
@POST: Returns repositories visible to PAT user.
"""
class MockGitService:
async def list_gitea_repositories(self, url, pat):
return [{"name": "test-repo", "full_name": "owner/test-repo", "private": True}]
return [
{"name": "test-repo", "full_name": "owner/test-repo", "private": True}
]
monkeypatch.setattr(git_routes, "git_service", MockGitService())
existing_config = GitServerConfig(
id="config-1", name="Gitea Server", provider=GitProvider.GITEA,
url="https://gitea.local", pat="gitea-token"
id="config-1",
name="Gitea Server",
provider=GitProvider.GITEA,
url="https://gitea.local",
pat="gitea-token",
)
db = DbMock([existing_config])
result = asyncio.run(git_routes.list_gitea_repositories(config_id="config-1", db=db))
result = asyncio.run(
git_routes.list_gitea_repositories(config_id="config-1", db=db)
)
assert len(result) == 1
assert result[0].name == "test-repo"
assert result[0].private is True
# [/DEF:test_list_gitea_repositories_returns_payload:Function]
# [DEF:test_list_gitea_repositories_rejects_non_gitea:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_list_gitea_repositories_rejects_non_gitea(monkeypatch):
"""
@PRE: config_id exists and provider is NOT GITEA.
@THROW: HTTPException 400
"""
existing_config = GitServerConfig(
id="config-1", name="GitHub Server", provider=GitProvider.GITHUB,
url="https://github.com", pat="token"
id="config-1",
name="GitHub Server",
provider=GitProvider.GITHUB,
url="https://github.com",
pat="token",
)
db = DbMock([existing_config])
with pytest.raises(HTTPException) as exc_info:
asyncio.run(git_routes.list_gitea_repositories(config_id="config-1", db=db))
assert exc_info.value.status_code == 400
assert "GITEA provider only" in exc_info.value.detail
# [/DEF:test_list_gitea_repositories_rejects_non_gitea:Function]
# [DEF:test_create_remote_repository_creates_provider_repo:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_create_remote_repository_creates_provider_repo(monkeypatch):
"""
@PRE: config_id exists and PAT has creation permissions.
@POST: Returns normalized remote repository payload.
"""
class MockGitService:
async def create_gitlab_repository(self, server_url, pat, name, private, description, auto_init, default_branch):
async def create_gitlab_repository(
self, server_url, pat, name, private, description, auto_init, default_branch
):
return {
"name": name,
"full_name": f"user/{name}",
"private": private,
"clone_url": f"{server_url}/user/{name}.git"
"clone_url": f"{server_url}/user/{name}.git",
}
monkeypatch.setattr(git_routes, "git_service", MockGitService())
from src.api.routes.git_schemas import RemoteRepoCreateRequest
existing_config = GitServerConfig(
id="config-1", name="GitLab Server", provider=GitProvider.GITLAB,
url="https://gitlab.com", pat="token"
id="config-1",
name="GitLab Server",
provider=GitProvider.GITLAB,
url="https://gitlab.com",
pat="token",
)
db = DbMock([existing_config])
request = RemoteRepoCreateRequest(name="new-repo", private=True, description="desc")
result = asyncio.run(git_routes.create_remote_repository(config_id="config-1", request=request, db=db))
result = asyncio.run(
git_routes.create_remote_repository(
config_id="config-1", request=request, db=db
)
)
assert result.provider == GitProvider.GITLAB
assert result.name == "new-repo"
assert result.full_name == "user/new-repo"
# [/DEF:test_create_remote_repository_creates_provider_repo:Function]
# [DEF:test_init_repository_initializes_and_saves_binding:Function]
# @RELATION: BINDS_TO ->[TestGitApi]
def test_init_repository_initializes_and_saves_binding(monkeypatch):
"""
@PRE: `dashboard_ref` exists and `init_data` contains valid config_id and remote_url.
@POST: Repository is initialized on disk and a GitRepository record is saved in DB.
"""
from src.api.routes.git_schemas import RepoInitRequest
class MockGitService:
def init_repo(self, dashboard_id, remote_url, pat, repo_key, default_branch):
self.init_called = True
def _get_repo_path(self, dashboard_id, repo_key):
return f"/tmp/repos/{repo_key}"
git_service_mock = MockGitService()
monkeypatch.setattr(git_routes, "git_service", git_service_mock)
monkeypatch.setattr(git_routes, "_resolve_dashboard_id_from_ref", lambda *args, **kwargs: 123)
monkeypatch.setattr(git_routes, "_resolve_repo_key_from_ref", lambda *args, **kwargs: "dashboard-123")
monkeypatch.setattr(
git_routes, "_resolve_dashboard_id_from_ref", lambda *args, **kwargs: 123
)
monkeypatch.setattr(
git_routes,
"_resolve_repo_key_from_ref",
lambda *args, **kwargs: "dashboard-123",
)
existing_config = GitServerConfig(
id="config-1", name="GitLab Server", provider=GitProvider.GITLAB,
url="https://gitlab.com", pat="token", default_branch="main"
id="config-1",
name="GitLab Server",
provider=GitProvider.GITLAB,
url="https://gitlab.com",
pat="token",
default_branch="main",
)
db = DbMock([existing_config])
init_data = RepoInitRequest(config_id="config-1", remote_url="https://git.local/repo.git")
result = asyncio.run(git_routes.init_repository(dashboard_ref="123", init_data=init_data, config_manager=MagicMock(), db=db))
init_data = RepoInitRequest(
config_id="config-1", remote_url="https://git.local/repo.git"
)
result = asyncio.run(
git_routes.init_repository(
dashboard_ref="123", init_data=init_data, config_manager=MagicMock(), db=db
)
)
assert result["status"] == "success"
assert git_service_mock.init_called is True
assert len(db._added) == 1
assert isinstance(db._added[0], GitRepository)
assert db._added[0].dashboard_id == 123
# [/DEF:backend.src.api.routes.__tests__.test_git_api:Module]
# [/DEF:test_init_repository_initializes_and_saves_binding:Function]
# [/DEF:TestGitApi:Module]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff