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>
300 lines
9.9 KiB
Python
300 lines
9.9 KiB
Python
"""
|
|
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
|