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>
252 lines
8.1 KiB
Python
252 lines
8.1 KiB
Python
"""
|
|
Tests for all Pydantic schemas.
|
|
"""
|
|
import pytest
|
|
from datetime import datetime
|
|
from app.schemas.common import SuccessResponse, ErrorResponse
|
|
from app.schemas.ssh_key import SshKeyCreate, SshKeyResponse
|
|
from app.schemas.server import ServerCreate, ServerUpdate, ServerResponse
|
|
from app.schemas.repo import RepoResponse, CommitInfo
|
|
from app.schemas.sync_log import SyncLogResponse
|
|
|
|
|
|
class TestCommonSchemas:
|
|
"""Test common response schemas."""
|
|
|
|
def test_success_response_with_data(self):
|
|
"""Test SuccessResponse with data."""
|
|
response = SuccessResponse(code=0, data={"id": 1}, message="success")
|
|
assert response.code == 0
|
|
assert response.data == {"id": 1}
|
|
assert response.message == "success"
|
|
|
|
def test_success_response_default_values(self):
|
|
"""Test SuccessResponse with default values."""
|
|
response = SuccessResponse(data={"test": "value"})
|
|
assert response.code == 0
|
|
assert response.message == "success"
|
|
|
|
def test_error_response(self):
|
|
"""Test ErrorResponse."""
|
|
response = ErrorResponse(code=404, message="Not found")
|
|
assert response.code == 404
|
|
assert response.message == "Not found"
|
|
assert response.data is None
|
|
|
|
def test_error_response_with_data(self):
|
|
"""Test ErrorResponse with additional data."""
|
|
response = ErrorResponse(
|
|
code=400,
|
|
message="Validation error",
|
|
data={"field": "name", "error": "required"}
|
|
)
|
|
assert response.code == 400
|
|
assert response.data == {"field": "name", "error": "required"}
|
|
|
|
|
|
class TestSshKeySchemas:
|
|
"""Test SSH key schemas."""
|
|
|
|
def test_ssh_key_create_valid(self):
|
|
"""Test SshKeyCreate with valid data."""
|
|
key = SshKeyCreate(
|
|
name="test-key",
|
|
private_key="-----BEGIN OPENSSH PRIVATE KEY-----\ntest\ntest\n-----END OPENSSH PRIVATE KEY-----"
|
|
)
|
|
assert key.name == "test-key"
|
|
assert "private_key" in key.model_dump()
|
|
|
|
def test_ssh_key_create_empty_name_fails(self):
|
|
"""Test SshKeyCreate fails with empty name."""
|
|
with pytest.raises(ValueError):
|
|
SshKeyCreate(
|
|
name=" ",
|
|
private_key="some-key"
|
|
)
|
|
|
|
def test_ssh_key_create_empty_private_key_fails(self):
|
|
"""Test SshKeyCreate fails with empty private_key."""
|
|
with pytest.raises(ValueError):
|
|
SshKeyCreate(
|
|
name="test-key",
|
|
private_key=""
|
|
)
|
|
|
|
def test_ssh_key_response(self):
|
|
"""Test SshKeyResponse."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
response = SshKeyResponse(
|
|
id=1,
|
|
name="test-key",
|
|
fingerprint="SHA256:abc123",
|
|
created_at=timestamp
|
|
)
|
|
assert response.id == 1
|
|
assert response.name == "test-key"
|
|
assert response.fingerprint == "SHA256:abc123"
|
|
assert response.created_at == timestamp
|
|
|
|
|
|
class TestServerSchemas:
|
|
"""Test server schemas."""
|
|
|
|
def test_server_create_valid(self):
|
|
"""Test ServerCreate with valid data."""
|
|
server = ServerCreate(
|
|
name="test-server",
|
|
url="https://gitea.example.com",
|
|
api_token="test-token",
|
|
ssh_key_id=1,
|
|
local_path="/data/test"
|
|
)
|
|
assert server.name == "test-server"
|
|
assert server.sync_enabled is False
|
|
assert server.schedule_cron is None
|
|
|
|
def test_server_create_with_sync(self):
|
|
"""Test ServerCreate with sync enabled."""
|
|
server = ServerCreate(
|
|
name="test-server",
|
|
url="https://gitea.example.com",
|
|
api_token="test-token",
|
|
ssh_key_id=1,
|
|
local_path="/data/test",
|
|
sync_enabled=True,
|
|
schedule_cron="0 */6 * * *"
|
|
)
|
|
assert server.sync_enabled is True
|
|
assert server.schedule_cron == "0 */6 * * *"
|
|
|
|
def test_server_create_invalid_ssh_key_id(self):
|
|
"""Test ServerCreate fails with invalid ssh_key_id."""
|
|
with pytest.raises(ValueError):
|
|
ServerCreate(
|
|
name="test-server",
|
|
url="https://gitea.example.com",
|
|
api_token="test-token",
|
|
ssh_key_id=0, # Must be > 0
|
|
local_path="/data/test"
|
|
)
|
|
|
|
def test_server_update_partial(self):
|
|
"""Test ServerUpdate with partial data."""
|
|
update = ServerUpdate(name="updated-name")
|
|
assert update.name == "updated-name"
|
|
assert update.url is None
|
|
assert update.api_token is None
|
|
|
|
def test_server_response(self):
|
|
"""Test ServerResponse."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
response = ServerResponse(
|
|
id=1,
|
|
name="test-server",
|
|
url="https://gitea.example.com",
|
|
ssh_key_id=1,
|
|
sync_enabled=True,
|
|
schedule_cron="0 */6 * * *",
|
|
local_path="/data/test",
|
|
status="success",
|
|
created_at=timestamp,
|
|
updated_at=timestamp
|
|
)
|
|
assert response.id == 1
|
|
assert response.status == "success"
|
|
assert response.sync_enabled is True
|
|
|
|
|
|
class TestRepoSchemas:
|
|
"""Test repository schemas."""
|
|
|
|
def test_commit_info(self):
|
|
"""Test CommitInfo schema."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
commit = CommitInfo(
|
|
hash="a1b2c3d4",
|
|
author="Test Author <test@example.com>",
|
|
message="Test commit",
|
|
timestamp=timestamp
|
|
)
|
|
assert commit.hash == "a1b2c3d4"
|
|
assert commit.author == "Test Author <test@example.com>"
|
|
assert commit.timestamp == timestamp
|
|
|
|
def test_repo_response(self):
|
|
"""Test RepoResponse schema."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
repo = RepoResponse(
|
|
id=1,
|
|
server_id=1,
|
|
name="test-repo",
|
|
full_name="org/test-repo",
|
|
clone_url="https://gitea.example.com/org/test-repo.git",
|
|
local_path="/data/test/org/test-repo",
|
|
last_sync_at=timestamp,
|
|
status="success",
|
|
created_at=timestamp
|
|
)
|
|
assert repo.id == 1
|
|
assert repo.name == "test-repo"
|
|
assert repo.last_sync_at == timestamp
|
|
assert repo.status == "success"
|
|
|
|
|
|
class TestSyncLogSchemas:
|
|
"""Test sync log schemas."""
|
|
|
|
def test_sync_log_response_success(self):
|
|
"""Test SyncLogResponse for successful sync."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
log = SyncLogResponse(
|
|
id=1,
|
|
repo_id=1,
|
|
status="success",
|
|
started_at=timestamp,
|
|
finished_at=timestamp + 300,
|
|
commits_count=5,
|
|
error_msg=None,
|
|
created_at=timestamp
|
|
)
|
|
assert log.id == 1
|
|
assert log.status == "success"
|
|
assert log.commits_count == 5
|
|
assert log.error_msg is None
|
|
|
|
def test_sync_log_response_error(self):
|
|
"""Test SyncLogResponse for failed sync."""
|
|
timestamp = int(datetime.utcnow().timestamp())
|
|
log = SyncLogResponse(
|
|
id=2,
|
|
repo_id=1,
|
|
status="error",
|
|
started_at=timestamp,
|
|
finished_at=timestamp + 60,
|
|
commits_count=None,
|
|
error_msg="Connection timeout",
|
|
created_at=timestamp
|
|
)
|
|
assert log.status == "error"
|
|
assert log.commits_count is None
|
|
assert log.error_msg == "Connection timeout"
|
|
|
|
|
|
class TestSchemaExports:
|
|
"""Test that all schemas are properly exported."""
|
|
|
|
def test_all_schemas_importable(self):
|
|
"""Test that all schemas can be imported from app.schemas."""
|
|
from app.schemas import (
|
|
SuccessResponse,
|
|
ErrorResponse,
|
|
SshKeyCreate,
|
|
SshKeyResponse,
|
|
ServerCreate,
|
|
ServerUpdate,
|
|
ServerResponse,
|
|
RepoResponse,
|
|
CommitInfo,
|
|
SyncLogResponse
|
|
)
|
|
# If we got here, all imports succeeded
|
|
assert True
|