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:
@@ -1,12 +1,13 @@
|
||||
import sys
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from typing import Generator
|
||||
|
||||
# Add backend to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
|
||||
# NOTE: This import will fail until models are created in Task 2.1
|
||||
# This is expected behavior - the models module doesn't exist yet
|
||||
@@ -58,3 +59,71 @@ def test_env_vars(db_path, test_encrypt_key, monkeypatch):
|
||||
"GM_ENCRYPT_KEY": test_encrypt_key,
|
||||
"GM_API_TOKEN": "test-token",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def client(db_session: Session, test_env_vars: dict, monkeypatch):
|
||||
"""
|
||||
FastAPI test client fixture.
|
||||
|
||||
Provides a test client for the FastAPI application with:
|
||||
- In-memory database session
|
||||
- Test environment variables
|
||||
- Disabled lifespan (for faster tests)
|
||||
|
||||
Args:
|
||||
db_session: Database session fixture
|
||||
test_env_vars: Test environment variables fixture
|
||||
monkeypatch: Pytest monkeypatch fixture
|
||||
|
||||
Yields:
|
||||
FastAPI test client
|
||||
|
||||
Example:
|
||||
def test_create_ssh_key(client):
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key", "private_key": "..."},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
assert response.status_code == 201
|
||||
"""
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
# Mock the lifespan context manager
|
||||
async def mock_lifespan(app): # noqa: ARG001 - Unused app parameter
|
||||
# Database is already initialized by db_session fixture
|
||||
yield
|
||||
|
||||
# Import after setting env vars
|
||||
import app.database
|
||||
import app.config
|
||||
|
||||
# Initialize database with test session
|
||||
app.database._engine = db_session.bind
|
||||
app.database._session_factory = sessionmaker(bind=db_session.bind, autocommit=False, autoflush=False)
|
||||
|
||||
# Create app with mocked lifespan
|
||||
from app.main import create_app
|
||||
test_app = create_app(lifespan_handler=mock_lifespan)
|
||||
|
||||
# Override get_db_session dependency to use test session
|
||||
from app.api import deps
|
||||
|
||||
def override_get_db_session():
|
||||
try:
|
||||
yield db_session
|
||||
finally:
|
||||
pass
|
||||
|
||||
test_app.dependency_overrides[deps.get_db_session] = override_get_db_session
|
||||
|
||||
with TestClient(test_app) as test_client:
|
||||
yield test_client
|
||||
|
||||
# Clean up
|
||||
test_app.dependency_overrides = {}
|
||||
app.database._engine = None
|
||||
app.database._session_factory = None
|
||||
app.config._settings = None
|
||||
|
||||
5
backend/tests/test_api/__init__.py
Normal file
5
backend/tests/test_api/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""
|
||||
Tests for API routes.
|
||||
|
||||
This package contains tests for all FastAPI route handlers.
|
||||
"""
|
||||
495
backend/tests/test_api/test_servers_api.py
Normal file
495
backend/tests/test_api/test_servers_api.py
Normal file
@@ -0,0 +1,495 @@
|
||||
"""
|
||||
Tests for Servers API routes.
|
||||
|
||||
Tests the following endpoints:
|
||||
- POST /api/servers
|
||||
- GET /api/servers
|
||||
- GET /api/servers/{id}
|
||||
- PUT /api/servers/{id}
|
||||
- DELETE /api/servers/{id}
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# Valid SSH private key for testing servers
|
||||
VALID_SSH_KEY = """-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAJC9AVH1vQFR
|
||||
AAAAAtzc2gtZWQyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQA
|
||||
AAECB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAHHZpXRvaXRvQVJNVjJH
|
||||
AAAAFGZpbGVzeXN0ZW0uY2ZnAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAEAAAAEWNvbW1lbnQ6IHRlc3Qga2V5AQIDBAUHBg=="""
|
||||
|
||||
|
||||
class TestCreateServer:
|
||||
"""Tests for POST /api/servers endpoint."""
|
||||
|
||||
def test_create_server_success(self, client: TestClient):
|
||||
"""Test creating a new server successfully."""
|
||||
# First create an SSH key
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
# Create server
|
||||
response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-api-token-123",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["message"] == "Server created successfully"
|
||||
assert data["data"]["name"] == "test-server"
|
||||
assert data["data"]["url"] == "https://gitea.example.com"
|
||||
assert data["data"]["ssh_key_id"] == ssh_key_id
|
||||
assert data["data"]["id"] > 0
|
||||
assert data["data"]["status"] == "untested"
|
||||
assert data["data"]["sync_enabled"] is False
|
||||
assert data["data"]["created_at"] > 0
|
||||
|
||||
def test_create_server_with_sync_enabled(self, client: TestClient):
|
||||
"""Test creating a server with sync enabled."""
|
||||
# Create SSH key
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-2", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
# Create server with sync enabled
|
||||
response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "sync-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-api-token",
|
||||
"ssh_key_id": ssh_key_id,
|
||||
"sync_enabled": True,
|
||||
"schedule_cron": "0 */6 * * *"
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["data"]["sync_enabled"] is True
|
||||
assert data["data"]["schedule_cron"] == "0 */6 * * *"
|
||||
|
||||
def test_create_server_duplicate_name(self, client: TestClient):
|
||||
"""Test creating a server with duplicate name fails."""
|
||||
# Create SSH key
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-3", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
# Create first server
|
||||
client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "duplicate-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Try to create duplicate
|
||||
response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "duplicate-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "already exists" in response.json()["detail"]
|
||||
|
||||
def test_create_server_invalid_ssh_key_id(self, client: TestClient):
|
||||
"""Test creating a server with non-existent SSH key fails."""
|
||||
response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": 99999 # Non-existent
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "not found" in response.json()["detail"]
|
||||
|
||||
def test_create_server_no_auth(self, client: TestClient):
|
||||
"""Test creating a server without authentication fails."""
|
||||
response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": 1
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestListServers:
|
||||
"""Tests for GET /api/servers endpoint."""
|
||||
|
||||
def test_list_servers_empty(self, client: TestClient):
|
||||
"""Test listing servers when none exist."""
|
||||
response = client.get(
|
||||
"/api/servers",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"] == []
|
||||
assert "0 server" in data["message"]
|
||||
|
||||
def test_list_servers_with_items(self, client: TestClient):
|
||||
"""Test listing servers when some exist."""
|
||||
# Create SSH key
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-list", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
# Create some servers
|
||||
client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "server-1",
|
||||
"url": "https://gitea1.example.com",
|
||||
"api_token": "token1",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "server-2",
|
||||
"url": "https://gitea2.example.com",
|
||||
"api_token": "token2",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/servers",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert len(data["data"]) == 2
|
||||
assert "api_token" not in data["data"][0] # Should not expose token
|
||||
|
||||
def test_list_servers_no_auth(self, client: TestClient):
|
||||
"""Test listing servers without authentication fails."""
|
||||
response = client.get("/api/servers")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestGetServer:
|
||||
"""Tests for GET /api/servers/{id} endpoint."""
|
||||
|
||||
def test_get_server_success(self, client: TestClient):
|
||||
"""Test getting a specific server successfully."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-get", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "get-test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Get the server
|
||||
response = client.get(
|
||||
f"/api/servers/{server_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"]["id"] == server_id
|
||||
assert data["data"]["name"] == "get-test-server"
|
||||
assert "api_token" not in data["data"]
|
||||
|
||||
def test_get_server_not_found(self, client: TestClient):
|
||||
"""Test getting a non-existent server fails."""
|
||||
response = client.get(
|
||||
"/api/servers/99999",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_get_server_no_auth(self, client: TestClient):
|
||||
"""Test getting a server without authentication fails."""
|
||||
response = client.get("/api/servers/1")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestUpdateServer:
|
||||
"""Tests for PUT /api/servers/{id} endpoint."""
|
||||
|
||||
def test_update_server_name(self, client: TestClient):
|
||||
"""Test updating a server's name."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-update", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "old-name",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Update name
|
||||
response = client.put(
|
||||
f"/api/servers/{server_id}",
|
||||
json={"name": "new-name"},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"]["name"] == "new-name"
|
||||
|
||||
def test_update_server_multiple_fields(self, client: TestClient):
|
||||
"""Test updating multiple server fields."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-multi", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Update multiple fields
|
||||
response = client.put(
|
||||
f"/api/servers/{server_id}",
|
||||
json={
|
||||
"sync_enabled": True,
|
||||
"schedule_cron": "0 */6 * * *",
|
||||
"status": "testing"
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["sync_enabled"] is True
|
||||
assert data["data"]["schedule_cron"] == "0 */6 * * *"
|
||||
assert data["data"]["status"] == "testing"
|
||||
|
||||
def test_update_server_api_token(self, client: TestClient):
|
||||
"""Test updating a server's API token."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-token", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "old-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Update API token
|
||||
response = client.put(
|
||||
f"/api/servers/{server_id}",
|
||||
json={"api_token": "new-token"},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_update_server_not_found(self, client: TestClient):
|
||||
"""Test updating a non-existent server fails."""
|
||||
response = client.put(
|
||||
"/api/servers/99999",
|
||||
json={"name": "new-name"},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_server_no_fields(self, client: TestClient):
|
||||
"""Test updating a server with no fields fails."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-nofield", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Update with empty body
|
||||
response = client.put(
|
||||
f"/api/servers/{server_id}",
|
||||
json={},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_update_server_no_auth(self, client: TestClient):
|
||||
"""Test updating a server without authentication fails."""
|
||||
response = client.put(
|
||||
"/api/servers/1",
|
||||
json={"name": "new-name"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestDeleteServer:
|
||||
"""Tests for DELETE /api/servers/{id} endpoint."""
|
||||
|
||||
def test_delete_server_success(self, client: TestClient):
|
||||
"""Test deleting a server successfully."""
|
||||
# Create SSH key and server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key-del", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
server_response = client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "delete-test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
server_id = server_response.json()["data"]["id"]
|
||||
|
||||
# Delete the server
|
||||
response = client.delete(
|
||||
f"/api/servers/{server_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["message"] == "Server deleted successfully"
|
||||
|
||||
# Verify it's deleted
|
||||
get_response = client.get(
|
||||
f"/api/servers/{server_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
def test_delete_server_not_found(self, client: TestClient):
|
||||
"""Test deleting a non-existent server fails."""
|
||||
response = client.delete(
|
||||
"/api/servers/99999",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_delete_server_no_auth(self, client: TestClient):
|
||||
"""Test deleting a server without authentication fails."""
|
||||
response = client.delete("/api/servers/1")
|
||||
|
||||
assert response.status_code == 401
|
||||
299
backend/tests/test_api/test_ssh_keys_api.py
Normal file
299
backend/tests/test_api/test_ssh_keys_api.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Tests for SSH Keys API routes.
|
||||
|
||||
Tests the following endpoints:
|
||||
- POST /api/ssh-keys
|
||||
- GET /api/ssh-keys
|
||||
- GET /api/ssh-keys/{id}
|
||||
- DELETE /api/ssh-keys/{id}
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
# Valid SSH private key for testing
|
||||
VALID_SSH_KEY = """-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAJC9AVH1vQFR
|
||||
AAAAAtzc2gtZWQyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQA
|
||||
AAECB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAHHZpXRvaXRvQVJNVjJH
|
||||
AAAAFGZpbGVzeXN0ZW0uY2ZnAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAEAAAAEWNvbW1lbnQ6IHRlc3Qga2V5AQIDBAUHBg=="""
|
||||
|
||||
# Invalid SSH key for testing
|
||||
INVALID_SSH_KEY = "not-a-valid-ssh-key"
|
||||
|
||||
|
||||
class TestCreateSshKey:
|
||||
"""Tests for POST /api/ssh-keys endpoint."""
|
||||
|
||||
def test_create_ssh_key_success(self, client: TestClient):
|
||||
"""Test creating a new SSH key successfully."""
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "test-key",
|
||||
"private_key": VALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["message"] == "SSH key created successfully"
|
||||
assert data["data"]["name"] == "test-key"
|
||||
assert data["data"]["id"] > 0
|
||||
assert data["data"]["fingerprint"] is not None
|
||||
assert data["data"]["created_at"] > 0
|
||||
|
||||
def test_create_ssh_key_duplicate_name(self, client: TestClient):
|
||||
"""Test creating a SSH key with duplicate name fails."""
|
||||
# Create first key
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "duplicate-key",
|
||||
"private_key": VALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Try to create duplicate
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "duplicate-key",
|
||||
"private_key": VALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "already exists" in data["detail"]
|
||||
|
||||
def test_create_ssh_key_invalid_key_format(self, client: TestClient):
|
||||
"""Test creating a SSH key with invalid format fails."""
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "invalid-key",
|
||||
"private_key": INVALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert "Invalid SSH private key format" in data["detail"]
|
||||
|
||||
def test_create_ssh_key_empty_name(self, client: TestClient):
|
||||
"""Test creating a SSH key with empty name fails."""
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "",
|
||||
"private_key": VALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
def test_create_ssh_key_no_auth(self, client: TestClient):
|
||||
"""Test creating a SSH key without authentication fails."""
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "test-key",
|
||||
"private_key": VALID_SSH_KEY
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_create_ssh_key_invalid_token(self, client: TestClient):
|
||||
"""Test creating a SSH key with invalid token fails."""
|
||||
response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={
|
||||
"name": "test-key",
|
||||
"private_key": VALID_SSH_KEY
|
||||
},
|
||||
headers={"Authorization": "Bearer invalid-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestListSshKeys:
|
||||
"""Tests for GET /api/ssh-keys endpoint."""
|
||||
|
||||
def test_list_ssh_keys_empty(self, client: TestClient):
|
||||
"""Test listing SSH keys when none exist."""
|
||||
response = client.get(
|
||||
"/api/ssh-keys",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"] == []
|
||||
assert "0 SSH key" in data["message"]
|
||||
|
||||
def test_list_ssh_keys_with_items(self, client: TestClient):
|
||||
"""Test listing SSH keys when some exist."""
|
||||
# Create some keys
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "key-1", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "key-2", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/ssh-keys",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert len(data["data"]) == 2
|
||||
assert data["data"][0]["name"] in ["key-1", "key-2"]
|
||||
assert "private_key" not in data["data"][0] # Should not expose private key
|
||||
|
||||
def test_list_ssh_keys_no_auth(self, client: TestClient):
|
||||
"""Test listing SSH keys without authentication fails."""
|
||||
response = client.get("/api/ssh-keys")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestGetSshKey:
|
||||
"""Tests for GET /api/ssh-keys/{id} endpoint."""
|
||||
|
||||
def test_get_ssh_key_success(self, client: TestClient):
|
||||
"""Test getting a specific SSH key successfully."""
|
||||
# Create a key first
|
||||
create_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "get-test-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
key_id = create_response.json()["data"]["id"]
|
||||
|
||||
# Get the key
|
||||
response = client.get(
|
||||
f"/api/ssh-keys/{key_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"]["id"] == key_id
|
||||
assert data["data"]["name"] == "get-test-key"
|
||||
assert "private_key" not in data["data"]
|
||||
|
||||
def test_get_ssh_key_not_found(self, client: TestClient):
|
||||
"""Test getting a non-existent SSH key fails."""
|
||||
response = client.get(
|
||||
"/api/ssh-keys/99999",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
assert "not found" in response.json()["detail"]
|
||||
|
||||
def test_get_ssh_key_no_auth(self, client: TestClient):
|
||||
"""Test getting an SSH key without authentication fails."""
|
||||
response = client.get("/api/ssh-keys/1")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestDeleteSshKey:
|
||||
"""Tests for DELETE /api/ssh-keys/{id} endpoint."""
|
||||
|
||||
def test_delete_ssh_key_success(self, client: TestClient):
|
||||
"""Test deleting an SSH key successfully."""
|
||||
# Create a key first
|
||||
create_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "delete-test-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
key_id = create_response.json()["data"]["id"]
|
||||
|
||||
# Delete the key
|
||||
response = client.delete(
|
||||
f"/api/ssh-keys/{key_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["message"] == "SSH key deleted successfully"
|
||||
|
||||
# Verify it's deleted
|
||||
get_response = client.get(
|
||||
f"/api/ssh-keys/{key_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
def test_delete_ssh_key_not_found(self, client: TestClient):
|
||||
"""Test deleting a non-existent SSH key fails."""
|
||||
response = client.delete(
|
||||
"/api/ssh-keys/99999",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_delete_ssh_key_in_use(self, client: TestClient):
|
||||
"""Test deleting an SSH key that is in use by a server fails."""
|
||||
# Create an SSH key
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "in-use-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
# Create a server using this key
|
||||
client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-api-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Try to delete the SSH key
|
||||
response = client.delete(
|
||||
f"/api/ssh-keys/{ssh_key_id}",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "in use" in response.json()["detail"].lower()
|
||||
|
||||
def test_delete_ssh_key_no_auth(self, client: TestClient):
|
||||
"""Test deleting an SSH key without authentication fails."""
|
||||
response = client.delete("/api/ssh-keys/1")
|
||||
|
||||
assert response.status_code == 401
|
||||
151
backend/tests/test_api/test_status_api.py
Normal file
151
backend/tests/test_api/test_status_api.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
Tests for Status API routes.
|
||||
|
||||
Tests the following endpoints:
|
||||
- GET /api/status
|
||||
- GET /api/status/health
|
||||
"""
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
VALID_SSH_KEY = """-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAJC9AVH1vQFR
|
||||
AAAAAtzc2gtZWQyNTUxOQAAACB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQA
|
||||
AAECB5WfkA8wgP/KvdGFNC1ZCbBmjZnKpM/LOXRDJS7NfRAQAAAHHZpXRvaXRvQVJNVjJH
|
||||
AAAAFGZpbGVzeXN0ZW0uY2ZnAAAAAQAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAA/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||
AAAAAAAAAAAAAAAEAAAAEWNvbW1lbnQ6IHRlc3Qga2V5AQIDBAUHBg=="""
|
||||
|
||||
|
||||
class TestGetStatus:
|
||||
"""Tests for GET /api/status endpoint."""
|
||||
|
||||
def test_get_status_unauthenticated(self, client: TestClient):
|
||||
"""Test getting status without authentication."""
|
||||
response = client.get("/api/status")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["message"] == "System status retrieved successfully"
|
||||
assert data["data"]["status"] == "healthy"
|
||||
assert data["data"]["version"] == "1.0.0"
|
||||
assert data["data"]["authenticated"] is False
|
||||
assert "database" in data["data"]
|
||||
assert data["data"]["database"]["status"] == "connected"
|
||||
# Storage paths should not be included for unauthenticated
|
||||
assert "storage" not in data["data"]
|
||||
|
||||
def test_get_status_authenticated(self, client: TestClient):
|
||||
"""Test getting status with authentication."""
|
||||
response = client.get(
|
||||
"/api/status",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"]["authenticated"] is True
|
||||
assert "storage" in data["data"]
|
||||
assert "data_dir" in data["data"]["storage"]
|
||||
assert "repos_dir" in data["data"]["storage"]
|
||||
assert "ssh_keys_dir" in data["data"]["storage"]
|
||||
assert "db_path" in data["data"]["storage"]
|
||||
|
||||
def test_get_status_with_data(self, client: TestClient):
|
||||
"""Test getting status when data exists."""
|
||||
# Create an SSH key
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "test-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Get status
|
||||
response = client.get(
|
||||
"/api/status",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["database"]["ssh_keys_count"] == 1
|
||||
assert data["data"]["database"]["servers_count"] == 0
|
||||
assert data["data"]["database"]["repos_count"] == 0
|
||||
|
||||
def test_get_status_database_counts(self, client: TestClient):
|
||||
"""Test database counts in status are accurate."""
|
||||
# Create multiple items
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "key-1", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "key-2", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Create a server
|
||||
ssh_response = client.post(
|
||||
"/api/ssh-keys",
|
||||
json={"name": "server-key", "private_key": VALID_SSH_KEY},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
ssh_key_id = ssh_response.json()["data"]["id"]
|
||||
|
||||
client.post(
|
||||
"/api/servers",
|
||||
json={
|
||||
"name": "test-server",
|
||||
"url": "https://gitea.example.com",
|
||||
"api_token": "test-token",
|
||||
"ssh_key_id": ssh_key_id
|
||||
},
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
# Get status
|
||||
response = client.get(
|
||||
"/api/status",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["data"]["database"]["ssh_keys_count"] == 3
|
||||
assert data["data"]["database"]["servers_count"] == 1
|
||||
|
||||
|
||||
class TestHealthCheck:
|
||||
"""Tests for GET /api/status/health endpoint."""
|
||||
|
||||
def test_health_check(self, client: TestClient):
|
||||
"""Test the health check endpoint."""
|
||||
response = client.get("/api/status/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["code"] == 0
|
||||
assert data["data"]["status"] == "ok"
|
||||
assert data["message"] == "Service is healthy"
|
||||
|
||||
def test_health_check_no_auth_required(self, client: TestClient):
|
||||
"""Test health check works without authentication."""
|
||||
response = client.get("/api/status/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_health_check_with_auth(self, client: TestClient):
|
||||
"""Test health check works with authentication."""
|
||||
response = client.get(
|
||||
"/api/status/health",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
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