Files
GitMa/backend/tests/test_services/test_sync_service.py
panw 44921c5646 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>
2026-03-30 16:30:13 +08:00

324 lines
12 KiB
Python

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