feat: complete Git Repo Manager MVP implementation
Backend (Phase 1-6): - Pydantic schemas for request/response validation - Service layer (SSH Key, Server, Repo, Sync) - API routes with authentication - FastAPI main application with lifespan management - ORM models (SshKey, Server, Repo, SyncLog) Frontend (Phase 7): - Vue 3 + Element Plus + Pinia + Vue Router - API client with Axios and interceptors - State management stores - All page components (Dashboard, Servers, Repos, SyncLogs, SshKeys, Settings) Deployment (Phase 8): - README with quick start guide - Startup script (start.sh) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
411
backend/tests/test_services/test_repo_service.py
Normal file
411
backend/tests/test_services/test_repo_service.py
Normal file
@@ -0,0 +1,411 @@
|
||||
"""
|
||||
Tests for Repo Service.
|
||||
"""
|
||||
import base64
|
||||
import pytest
|
||||
import time
|
||||
from pathlib import Path
|
||||
from app.models.repo import Repo
|
||||
from app.models.server import Server
|
||||
from app.services.repo_service import RepoService
|
||||
from app.services.server_service import ServerService
|
||||
from app.services.ssh_key_service import SshKeyService
|
||||
|
||||
|
||||
# Valid test SSH key
|
||||
VALID_SSH_KEY = """-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB/pDNwjNcznNaRlLNF5G9hCQNjbqNZ7QeKyLIy/nvHAAAAJi/vqmQv6pk
|
||||
AAAAAtzc2gtZWQyNTUxOQAAACB/pDNwjNcznNaRlLNF5G9hCQNjbqNZ7QeKyLIy/nvHAA
|
||||
AAAEBD0cWNQnpLDUYEGNMSgVIApVJfCFuRfGG3uxJZRKLvqH+kM3CM1zOc1pGUssXkb2E
|
||||
JA2uuo1ntB4rIsjL+e8cAAAADm1lc3NlbmdlckBrZW50cm9zBAgMEBQ=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
|
||||
def create_test_server_with_repo(db_session, server_name="test-server"):
|
||||
"""Helper to create a test server."""
|
||||
ssh_service = SshKeyService(db_session)
|
||||
ssh_key = ssh_service.create_ssh_key(name="test-ssh-key", private_key=VALID_SSH_KEY)
|
||||
|
||||
server_service = ServerService(db_session)
|
||||
return server_service.create_server(
|
||||
name=server_name,
|
||||
url="https://gitea.example.com",
|
||||
api_token="test-api-token",
|
||||
ssh_key_id=ssh_key.id,
|
||||
sync_enabled=False,
|
||||
schedule_cron=None
|
||||
)
|
||||
|
||||
|
||||
class TestCreateRepo:
|
||||
"""Tests for create_repo method."""
|
||||
|
||||
def test_create_repo_success(self, db_session, test_env_vars):
|
||||
"""Test successful repository creation."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="test-repo",
|
||||
full_name="owner/test-repo",
|
||||
clone_url="git@gitea.example.com:owner/test-repo.git",
|
||||
local_path="/tmp/test-repo"
|
||||
)
|
||||
|
||||
assert repo.id is not None
|
||||
assert repo.server_id == server.id
|
||||
assert repo.name == "test-repo"
|
||||
assert repo.full_name == "owner/test-repo"
|
||||
assert repo.clone_url == "git@gitea.example.com:owner/test-repo.git"
|
||||
assert repo.local_path == "/tmp/test-repo"
|
||||
assert repo.status == "pending"
|
||||
assert repo.last_sync_at is None
|
||||
assert repo.created_at is not None
|
||||
|
||||
def test_create_repo_with_invalid_server_id(self, db_session, test_env_vars):
|
||||
"""Test that invalid server_id is rejected."""
|
||||
service = RepoService(db_session)
|
||||
|
||||
with pytest.raises(ValueError, match="Server not found"):
|
||||
service.create_repo(
|
||||
server_id=99999,
|
||||
name="test-repo",
|
||||
full_name="owner/test-repo",
|
||||
clone_url="git@gitea.example.com:owner/test-repo.git",
|
||||
local_path="/tmp/test-repo"
|
||||
)
|
||||
|
||||
def test_create_repo_generates_local_path(self, db_session, test_env_vars):
|
||||
"""Test that local_path can be generated correctly."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
# Test with None local_path to let it generate
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="auto-path-repo",
|
||||
full_name="owner/auto-path-repo",
|
||||
clone_url="git@gitea.example.com:owner/auto-path-repo.git",
|
||||
local_path=None
|
||||
)
|
||||
|
||||
# Should generate path based on server and repo name
|
||||
assert repo.local_path is not None
|
||||
assert "auto-path-repo" in repo.local_path
|
||||
|
||||
def test_create_repo_duplicate_name_same_server(self, db_session, test_env_vars):
|
||||
"""Test that duplicate repo names on same server are allowed (different from servers)."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
# Create first repo
|
||||
service.create_repo(
|
||||
server_id=server.id,
|
||||
name="duplicate-repo",
|
||||
full_name="owner1/duplicate-repo",
|
||||
clone_url="git@gitea.example.com:owner1/duplicate-repo.git",
|
||||
local_path="/tmp/duplicate-repo-1"
|
||||
)
|
||||
|
||||
# Create second repo with same name but different full_name
|
||||
repo2 = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="duplicate-repo",
|
||||
full_name="owner2/duplicate-repo",
|
||||
clone_url="git@gitea.example.com:owner2/duplicate-repo.git",
|
||||
local_path="/tmp/duplicate-repo-2"
|
||||
)
|
||||
|
||||
assert repo2 is not None
|
||||
assert repo2.full_name == "owner2/duplicate-repo"
|
||||
|
||||
def test_create_repo_default_status(self, db_session, test_env_vars):
|
||||
"""Test that new repos have default 'pending' status."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="status-test-repo",
|
||||
full_name="owner/status-test-repo",
|
||||
clone_url="git@gitea.example.com:owner/status-test-repo.git",
|
||||
local_path="/tmp/status-test-repo"
|
||||
)
|
||||
|
||||
assert repo.status == "pending"
|
||||
|
||||
|
||||
class TestListRepos:
|
||||
"""Tests for list_repos method."""
|
||||
|
||||
def test_list_repos_empty(self, db_session, test_env_vars):
|
||||
"""Test listing repos when none exist."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repos = service.list_repos(server.id)
|
||||
|
||||
assert repos == []
|
||||
|
||||
def test_list_repos_multiple(self, db_session, test_env_vars):
|
||||
"""Test listing multiple repos for a server."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
service.create_repo(
|
||||
server_id=server.id,
|
||||
name="repo-1",
|
||||
full_name="owner/repo-1",
|
||||
clone_url="git@gitea.example.com:owner/repo-1.git",
|
||||
local_path="/tmp/repo-1"
|
||||
)
|
||||
service.create_repo(
|
||||
server_id=server.id,
|
||||
name="repo-2",
|
||||
full_name="owner/repo-2",
|
||||
clone_url="git@gitea.example.com:owner/repo-2.git",
|
||||
local_path="/tmp/repo-2"
|
||||
)
|
||||
|
||||
repos = service.list_repos(server.id)
|
||||
|
||||
assert len(repos) == 2
|
||||
assert any(r.name == "repo-1" for r in repos)
|
||||
assert any(r.name == "repo-2" for r in repos)
|
||||
|
||||
def test_list_repos_filters_by_server(self, db_session, test_env_vars):
|
||||
"""Test that list_repos only returns repos for specified server."""
|
||||
server1 = create_test_server_with_repo(db_session, "server-1")
|
||||
server2 = create_test_server_with_repo(db_session, "server-2")
|
||||
service = RepoService(db_session)
|
||||
|
||||
# Create repo for server1
|
||||
service.create_repo(
|
||||
server_id=server1.id,
|
||||
name="server1-repo",
|
||||
full_name="owner/server1-repo",
|
||||
clone_url="git@gitea.example.com:owner/server1-repo.git",
|
||||
local_path="/tmp/server1-repo"
|
||||
)
|
||||
|
||||
# Create repo for server2
|
||||
service.create_repo(
|
||||
server_id=server2.id,
|
||||
name="server2-repo",
|
||||
full_name="owner/server2-repo",
|
||||
clone_url="git@gitea.example.com:owner/server2-repo.git",
|
||||
local_path="/tmp/server2-repo"
|
||||
)
|
||||
|
||||
# List repos for server1 should only return server1's repo
|
||||
server1_repos = service.list_repos(server1.id)
|
||||
assert len(server1_repos) == 1
|
||||
assert server1_repos[0].name == "server1-repo"
|
||||
|
||||
# List repos for server2 should only return server2's repo
|
||||
server2_repos = service.list_repos(server2.id)
|
||||
assert len(server2_repos) == 1
|
||||
assert server2_repos[0].name == "server2-repo"
|
||||
|
||||
def test_list_repos_ordered_by_creation(self, db_session, test_env_vars):
|
||||
"""Test that repos are listed in creation order."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo1 = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="first-repo",
|
||||
full_name="owner/first-repo",
|
||||
clone_url="git@gitea.example.com:owner/first-repo.git",
|
||||
local_path="/tmp/first-repo"
|
||||
)
|
||||
|
||||
time.sleep(0.1) # Small delay to ensure timestamp difference
|
||||
|
||||
repo2 = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="second-repo",
|
||||
full_name="owner/second-repo",
|
||||
clone_url="git@gitea.example.com:owner/second-repo.git",
|
||||
local_path="/tmp/second-repo"
|
||||
)
|
||||
|
||||
repos = service.list_repos(server.id)
|
||||
|
||||
assert repos[0].id == repo1.id
|
||||
assert repos[1].id == repo2.id
|
||||
|
||||
|
||||
class TestGetRepo:
|
||||
"""Tests for get_repo method."""
|
||||
|
||||
def test_get_repo_by_id(self, db_session, test_env_vars):
|
||||
"""Test getting a repo by ID."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
created_repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="get-test-repo",
|
||||
full_name="owner/get-test-repo",
|
||||
clone_url="git@gitea.example.com:owner/get-test-repo.git",
|
||||
local_path="/tmp/get-test-repo"
|
||||
)
|
||||
|
||||
retrieved_repo = service.get_repo(created_repo.id)
|
||||
|
||||
assert retrieved_repo is not None
|
||||
assert retrieved_repo.id == created_repo.id
|
||||
assert retrieved_repo.name == "get-test-repo"
|
||||
|
||||
def test_get_repo_not_found(self, db_session, test_env_vars):
|
||||
"""Test getting a non-existent repo."""
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.get_repo(99999)
|
||||
|
||||
assert repo is None
|
||||
|
||||
|
||||
class TestUpdateRepoStatus:
|
||||
"""Tests for update_repo_status method."""
|
||||
|
||||
def test_update_repo_status_to_syncing(self, db_session, test_env_vars):
|
||||
"""Test updating repo status to syncing."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="status-repo",
|
||||
full_name="owner/status-repo",
|
||||
clone_url="git@gitea.example.com:owner/status-repo.git",
|
||||
local_path="/tmp/status-repo"
|
||||
)
|
||||
|
||||
updated_repo = service.update_repo_status(repo.id, "syncing")
|
||||
|
||||
assert updated_repo.status == "syncing"
|
||||
assert updated_repo.id == repo.id
|
||||
|
||||
def test_update_repo_status_to_success(self, db_session, test_env_vars):
|
||||
"""Test updating repo status to success."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="status-repo",
|
||||
full_name="owner/status-repo",
|
||||
clone_url="git@gitea.example.com:owner/status-repo.git",
|
||||
local_path="/tmp/status-repo"
|
||||
)
|
||||
|
||||
updated_repo = service.update_repo_status(repo.id, "success")
|
||||
|
||||
assert updated_repo.status == "success"
|
||||
|
||||
def test_update_repo_status_to_failed(self, db_session, test_env_vars):
|
||||
"""Test updating repo status to failed."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="status-repo",
|
||||
full_name="owner/status-repo",
|
||||
clone_url="git@gitea.example.com:owner/status-repo.git",
|
||||
local_path="/tmp/status-repo"
|
||||
)
|
||||
|
||||
updated_repo = service.update_repo_status(repo.id, "failed")
|
||||
|
||||
assert updated_repo.status == "failed"
|
||||
|
||||
def test_update_repo_status_not_found(self, db_session, test_env_vars):
|
||||
"""Test updating status for non-existent repo."""
|
||||
service = RepoService(db_session)
|
||||
|
||||
with pytest.raises(ValueError, match="Repo not found"):
|
||||
service.update_repo_status(99999, "syncing")
|
||||
|
||||
|
||||
class TestDeleteRepo:
|
||||
"""Tests for delete_repo method."""
|
||||
|
||||
def test_delete_repo_success(self, db_session, test_env_vars):
|
||||
"""Test successful repo deletion."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="delete-test-repo",
|
||||
full_name="owner/delete-test-repo",
|
||||
clone_url="git@gitea.example.com:owner/delete-test-repo.git",
|
||||
local_path="/tmp/delete-test-repo"
|
||||
)
|
||||
|
||||
result = service.delete_repo(repo.id)
|
||||
|
||||
assert result is True
|
||||
|
||||
# Verify the repo is deleted
|
||||
retrieved_repo = service.get_repo(repo.id)
|
||||
assert retrieved_repo is None
|
||||
|
||||
def test_delete_repo_not_found(self, db_session, test_env_vars):
|
||||
"""Test deleting a non-existent repo."""
|
||||
service = RepoService(db_session)
|
||||
|
||||
result = service.delete_repo(99999)
|
||||
|
||||
assert result is False
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for Repo service."""
|
||||
|
||||
def test_repo_lifecycle(self, db_session, test_env_vars):
|
||||
"""Test complete lifecycle of a repo."""
|
||||
server = create_test_server_with_repo(db_session)
|
||||
service = RepoService(db_session)
|
||||
|
||||
# Create repo
|
||||
repo = service.create_repo(
|
||||
server_id=server.id,
|
||||
name="lifecycle-repo",
|
||||
full_name="owner/lifecycle-repo",
|
||||
clone_url="git@gitea.example.com:owner/lifecycle-repo.git",
|
||||
local_path="/tmp/lifecycle-repo"
|
||||
)
|
||||
assert repo.status == "pending"
|
||||
|
||||
# Update status to syncing
|
||||
repo = service.update_repo_status(repo.id, "syncing")
|
||||
assert repo.status == "syncing"
|
||||
|
||||
# Update status to success
|
||||
repo = service.update_repo_status(repo.id, "success")
|
||||
assert repo.status == "success"
|
||||
|
||||
# Verify we can retrieve it
|
||||
retrieved = service.get_repo(repo.id)
|
||||
assert retrieved is not None
|
||||
assert retrieved.status == "success"
|
||||
|
||||
# List repos for server
|
||||
repos = service.list_repos(server.id)
|
||||
assert len(repos) == 1
|
||||
assert repos[0].id == repo.id
|
||||
|
||||
# Delete repo
|
||||
result = service.delete_repo(repo.id)
|
||||
assert result is True
|
||||
|
||||
# Verify it's gone
|
||||
repos = service.list_repos(server.id)
|
||||
assert len(repos) == 0
|
||||
323
backend/tests/test_services/test_sync_service.py
Normal file
323
backend/tests/test_services/test_sync_service.py
Normal file
@@ -0,0 +1,323 @@
|
||||
"""
|
||||
Tests for Sync Service.
|
||||
"""
|
||||
import base64
|
||||
import pytest
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock, call
|
||||
from app.models.repo import Repo
|
||||
from app.models.server import Server
|
||||
from app.models.ssh_key import SshKey
|
||||
from app.services.sync_service import SyncService
|
||||
|
||||
|
||||
# Valid test SSH key
|
||||
VALID_SSH_KEY = """-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB/pDNwjNcznNaRlLNF5G9hCQNjbqNZ7QeKyLIy/nvHAAAAJi/vqmQv6pk
|
||||
AAAAAtzc2gtZWQyNTUxOQAAACB/pDNwjNcznNaRlLNF5G9hCQNjbqNZ7QeKyLIy/nvHAA
|
||||
AAAEBD0cWNQnpLDUYEGNMSgVIApVJfCFuRfGG3uxJZRKLvqH+kM3CM1zOc1pGUssXkb2E
|
||||
JA2uuo1ntB4rIsjL+e8cAAAADm1lc3NlbmdlckBrZW50cm9zBAgMEBQ=
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
|
||||
def create_test_server(db_session, name="test-server"):
|
||||
"""Helper to create a test server."""
|
||||
from app.services.ssh_key_service import SshKeyService
|
||||
from app.services.server_service import ServerService
|
||||
|
||||
ssh_service = SshKeyService(db_session)
|
||||
ssh_key = ssh_service.create_ssh_key(name="test-ssh-key", private_key=VALID_SSH_KEY)
|
||||
|
||||
server_service = ServerService(db_session)
|
||||
return server_service.create_server(
|
||||
name=name,
|
||||
url="https://gitea.example.com",
|
||||
api_token="test-api-token",
|
||||
ssh_key_id=ssh_key.id,
|
||||
sync_enabled=False,
|
||||
schedule_cron=None
|
||||
)
|
||||
|
||||
|
||||
def create_test_repo(db_session, server_id=None, name="test-repo", local_path=None):
|
||||
"""Helper to create a test repo."""
|
||||
if server_id is None:
|
||||
server = create_test_server(db_session)
|
||||
server_id = server.id
|
||||
|
||||
import time
|
||||
repo = Repo(
|
||||
server_id=server_id,
|
||||
name=name,
|
||||
full_name=f"owner/{name}",
|
||||
clone_url="git@gitea.example.com:owner/test-repo.git",
|
||||
local_path=local_path or "/tmp/test-repo",
|
||||
status="pending",
|
||||
created_at=int(time.time())
|
||||
)
|
||||
db_session.add(repo)
|
||||
db_session.commit()
|
||||
db_session.refresh(repo)
|
||||
return repo
|
||||
|
||||
|
||||
class TestSyncRepo:
|
||||
"""Tests for sync_repo method."""
|
||||
|
||||
def test_sync_repo_clones_new_repo(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test syncing a new repository triggers clone."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path / "test-repo"))
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch.object(service, '_clone_repo') as mock_clone:
|
||||
service.sync_repo(repo, VALID_SSH_KEY)
|
||||
mock_clone.assert_called_once()
|
||||
|
||||
def test_sync_repo_fetches_existing_repo(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test syncing an existing repository triggers fetch."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path / "test-repo"))
|
||||
# Create the directory to simulate existing repo
|
||||
tmp_path.joinpath("test-repo").mkdir()
|
||||
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch.object(service, '_fetch_repo') as mock_fetch:
|
||||
with patch('pathlib.Path.exists', return_value=True):
|
||||
service.sync_repo(repo, VALID_SSH_KEY)
|
||||
mock_fetch.assert_called_once()
|
||||
|
||||
def test_sync_repo_updates_status_to_syncing(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that sync_repo updates repo status to syncing."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path / "test-repo"))
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch.object(service, '_clone_repo'):
|
||||
service.sync_repo(repo, VALID_SSH_KEY)
|
||||
assert repo.status == "syncing"
|
||||
|
||||
def test_sync_repo_updates_last_sync_at(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that sync_repo updates last_sync_at timestamp."""
|
||||
import time
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path / "test-repo"))
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch.object(service, '_clone_repo'):
|
||||
before_sync = int(time.time())
|
||||
service.sync_repo(repo, VALID_SSH_KEY)
|
||||
after_sync = int(time.time())
|
||||
|
||||
assert before_sync <= repo.last_sync_at <= after_sync
|
||||
|
||||
|
||||
class TestCloneRepo:
|
||||
"""Tests for _clone_repo method."""
|
||||
|
||||
def test_clone_repo_calls_git_command(self, db_session, test_env_vars):
|
||||
"""Test that _clone_repo calls git clone with correct arguments."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
service._clone_repo(
|
||||
"git@gitea.example.com:owner/repo.git",
|
||||
"/tmp/repo",
|
||||
VALID_SSH_KEY
|
||||
)
|
||||
|
||||
# Verify subprocess.run was called
|
||||
assert mock_run.called
|
||||
call_args = mock_run.call_args
|
||||
assert 'git' in call_args[0][0]
|
||||
|
||||
def test_clone_repo_creates_ssh_wrapper(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that _clone_repo creates SSH key wrapper."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
with patch('tempfile.NamedTemporaryFile') as mock_temp:
|
||||
mock_temp.return_value.__enter__.return_value.name = "/tmp/test_key"
|
||||
|
||||
service._clone_repo(
|
||||
"git@gitea.example.com:owner/repo.git",
|
||||
str(tmp_path / "repo"),
|
||||
VALID_SSH_KEY
|
||||
)
|
||||
|
||||
# Verify temp file was created
|
||||
mock_temp.assert_called()
|
||||
|
||||
def test_clone_repo_handles_failure(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that _clone_repo handles git clone failures."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'git')
|
||||
|
||||
with pytest.raises(Exception):
|
||||
service._clone_repo(
|
||||
"git@gitea.example.com:owner/repo.git",
|
||||
str(tmp_path / "repo"),
|
||||
VALID_SSH_KEY
|
||||
)
|
||||
|
||||
|
||||
class TestFetchRepo:
|
||||
"""Tests for _fetch_repo method."""
|
||||
|
||||
def test_fetch_repo_calls_git_fetch(self, db_session, test_env_vars):
|
||||
"""Test that _fetch_repo calls git fetch --all."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(returncode=0)
|
||||
|
||||
service._fetch_repo("/tmp/repo", VALID_SSH_KEY)
|
||||
|
||||
# Verify subprocess.run was called
|
||||
assert mock_run.called
|
||||
call_args = mock_run.call_args
|
||||
assert 'git' in call_args[0][0]
|
||||
|
||||
def test_fetch_repo_handles_failure(self, db_session, test_env_vars):
|
||||
"""Test that _fetch_repo handles git fetch failures."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'git')
|
||||
|
||||
with pytest.raises(Exception):
|
||||
service._fetch_repo("/tmp/repo", VALID_SSH_KEY)
|
||||
|
||||
|
||||
class TestCountCommits:
|
||||
"""Tests for _count_commits method."""
|
||||
|
||||
def test_count_commits_returns_zero_for_nonexistent_repo(self, db_session, test_env_vars):
|
||||
"""Test that _count_commits returns 0 for non-existent repo."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
count = service._count_commits("/nonexistent/repo")
|
||||
|
||||
assert count == 0
|
||||
|
||||
def test_count_commits_parses_git_output(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that _count_commits parses git rev-list output."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
# Simulate git output with commit count
|
||||
result = Mock()
|
||||
result.stdout.decode.return_value = "a1b2c3d\ne4f5g6h\n"
|
||||
result.returncode = 0
|
||||
mock_run.return_value = result
|
||||
|
||||
count = service._count_commits(str(tmp_path))
|
||||
|
||||
assert count == 2
|
||||
|
||||
def test_count_commits_handles_command_failure(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that _count_commits handles git command failure."""
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'git')
|
||||
|
||||
count = service._count_commits(str(tmp_path))
|
||||
|
||||
assert count == 0
|
||||
|
||||
|
||||
class TestGetRepoCommits:
|
||||
"""Tests for get_repo_commits method."""
|
||||
|
||||
def test_get_repo_commits_returns_empty_list_for_nonexistent_repo(self, db_session, test_env_vars):
|
||||
"""Test that get_repo_commits returns empty list for non-existent repo."""
|
||||
repo = create_test_repo(db_session, local_path="/nonexistent/repo")
|
||||
service = SyncService(db_session)
|
||||
|
||||
commits = service.get_repo_commits(repo, limit=10)
|
||||
|
||||
assert commits == []
|
||||
|
||||
def test_get_repo_commits_parses_git_log_output(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that get_repo_commits parses git log output."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path))
|
||||
service = SyncService(db_session)
|
||||
|
||||
git_log_output = """a1b2c3d4|First commit|author1|author1@example.com|1000000
|
||||
e4f5g6h7|Second commit|author2|author2@example.com|2000000"""
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
result = Mock()
|
||||
result.stdout.decode.return_value = git_log_output
|
||||
result.returncode = 0
|
||||
mock_run.return_value = result
|
||||
|
||||
commits = service.get_repo_commits(repo, limit=10)
|
||||
|
||||
assert len(commits) == 2
|
||||
assert commits[0]['hash'] == 'a1b2c3d4'
|
||||
assert commits[0]['message'] == 'First commit'
|
||||
assert commits[1]['hash'] == 'e4f5g6h7'
|
||||
assert commits[1]['message'] == 'Second commit'
|
||||
|
||||
def test_get_repo_commits_respects_limit(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that get_repo_commits respects the limit parameter."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path))
|
||||
service = SyncService(db_session)
|
||||
|
||||
git_log_output = """a1b2c3d4|Commit 1|author1|author1@example.com|1000000
|
||||
e4f5g6h7|Commit 2|author2|author2@example.com|2000000
|
||||
i7j8k9l0|Commit 3|author3|author3@example.com|3000000"""
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
result = Mock()
|
||||
result.stdout.decode.return_value = git_log_output
|
||||
result.returncode = 0
|
||||
mock_run.return_value = result
|
||||
|
||||
commits = service.get_repo_commits(repo, limit=2)
|
||||
|
||||
assert len(commits) == 2
|
||||
|
||||
def test_get_repo_commits_handles_command_failure(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test that get_repo_commits handles git command failure."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path))
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, 'git')
|
||||
|
||||
commits = service.get_repo_commits(repo, limit=10)
|
||||
|
||||
assert commits == []
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for Sync service."""
|
||||
|
||||
def test_sync_and_get_commits_workflow(self, db_session, test_env_vars, tmp_path):
|
||||
"""Test complete workflow of syncing and getting commits."""
|
||||
repo = create_test_repo(db_session, local_path=str(tmp_path / "repo"))
|
||||
service = SyncService(db_session)
|
||||
|
||||
with patch.object(service, '_clone_repo'):
|
||||
service.sync_repo(repo, VALID_SSH_KEY)
|
||||
|
||||
# Simulate commits exist
|
||||
git_log_output = "a1b2c3d4|Test commit|author|author@example.com|1000000"
|
||||
with patch('subprocess.run') as mock_run:
|
||||
result = Mock()
|
||||
result.stdout.decode.return_value = git_log_output
|
||||
result.returncode = 0
|
||||
mock_run.return_value = result
|
||||
|
||||
commits = service.get_repo_commits(repo, limit=10)
|
||||
|
||||
assert len(commits) == 1
|
||||
assert repo.status == "syncing"
|
||||
Reference in New Issue
Block a user