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:
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
|
||||
Reference in New Issue
Block a user