feat: add web UI with login, CRUD, admin, and API key management

- Add login page with JWT authentication
- Add dashboard with stats and quick actions
- Add links management page (full CRUD with search)
- Add collections management page
- Add API key management page with copy-to-clipboard
- Add admin user management page (admin only)
- Fix UUID type mismatches across all endpoints
- Add updated_at column to api_keys and audit_log in schema.sql
- Fix DB_PASSWORD default in docker-compose.yml
- Add PyJWT to requirements.txt
- Fix API docs URL (/docs instead of /api/docs)
- Improve JS error handling (show actual messages)
- Rewrite conftest.py with proper DB lifecycle management
- Add 42 new integration tests (84 total, all passing)
  - test_admin.py: 15 tests for admin endpoints
  - test_auth_extended.py: 9 tests for API key CRUD
  - test_tags.py: 12 tests for tag endpoints
  - test_sync.py: 6 tests for sync endpoints
This commit is contained in:
DavidSaylor
2026-05-21 07:21:49 -05:00
parent 09d30427f4
commit 77b076c7d7
31 changed files with 2740 additions and 213 deletions

View File

@@ -0,0 +1,223 @@
"""
LinkSyncServer - Admin API Tests
"""
import uuid
import pytest
from fastapi.testclient import TestClient
class TestAdmin:
def test_list_users(self, client: TestClient, admin_token: str):
response = client.get(
"/api/admin/users",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_user(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
response = client.post(
"/api/admin/users",
json={
"username": f"admin_created_{unique}",
"email": f"admin_{unique}@example.com",
"password": "testpass123",
"role": "user",
"is_active": True,
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 201
data = response.json()
assert data["username"] == f"admin_created_{unique}"
def test_create_admin_user(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
response = client.post(
"/api/admin/users",
json={
"username": f"admin_user_{unique}",
"email": f"adminuser_{unique}@example.com",
"password": "testpass123",
"role": "admin",
"is_active": True,
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 201
assert response.json()["role"] == "admin"
def test_create_user_duplicate(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
client.post(
"/api/admin/users",
json={
"username": f"dup_admin_{unique}",
"email": f"dupadmin_{unique}@example.com",
"password": "testpass123",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
response = client.post(
"/api/admin/users",
json={
"username": f"dup_admin_{unique}",
"email": f"dupadmin2_{unique}@example.com",
"password": "testpass123",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 400
def test_get_user(self, client: TestClient, admin_token: str):
users = client.get("/api/admin/users", headers={"Authorization": f"Bearer {admin_token}"}).json()
if users:
user_id = users[0]["id"]
response = client.get(
f"/api/admin/users/{user_id}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert "username" in response.json()
def test_get_user_not_found(self, client: TestClient, admin_token: str):
response = client.get(
"/api/admin/users/00000000-0000-0000-0000-000000000000",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 404
def test_update_user(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
create_resp = client.post(
"/api/admin/users",
json={
"username": f"update_user_{unique}",
"email": f"update_{unique}@example.com",
"password": "testpass123",
"role": "user",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
user_id = create_resp.json()["id"]
response = client.put(
f"/api/admin/users/{user_id}",
json={"email": f"updated_{unique}@example.com"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert response.json()["email"] == f"updated_{unique}@example.com"
def test_update_user_role(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
create_resp = client.post(
"/api/admin/users",
json={
"username": f"role_user_{unique}",
"email": f"role_{unique}@example.com",
"password": "testpass123",
"role": "user",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
user_id = create_resp.json()["id"]
response = client.put(
f"/api/admin/users/{user_id}",
json={"role": "admin"},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert response.json()["role"] == "admin"
def test_deactivate_user(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
create_resp = client.post(
"/api/admin/users",
json={
"username": f"deact_user_{unique}",
"email": f"deact_{unique}@example.com",
"password": "testpass123",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
user_id = create_resp.json()["id"]
response = client.put(
f"/api/admin/users/{user_id}",
json={"is_active": False},
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert response.json()["is_active"] is False
def test_delete_user(self, client: TestClient, admin_token: str):
unique = str(uuid.uuid4())[:8]
create_resp = client.post(
"/api/admin/users",
json={
"username": f"del_user_{unique}",
"email": f"del_{unique}@example.com",
"password": "testpass123",
},
headers={"Authorization": f"Bearer {admin_token}"},
)
user_id = create_resp.json()["id"]
response = client.delete(
f"/api/admin/users/{user_id}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
def test_delete_self_not_allowed(self, client: TestClient, admin_token: str):
users = client.get("/api/admin/users", headers={"Authorization": f"Bearer {admin_token}"}).json()
admin_user = next((u for u in users if u["username"] == "admin"), None)
if admin_user:
response = client.delete(
f"/api/admin/users/{admin_user['id']}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 400
def test_stats(self, client: TestClient, admin_token: str):
response = client.get(
"/api/admin/stats",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert "total_users" in data
assert "total_bookmarks" in data
def test_audit_log(self, client: TestClient, admin_token: str):
response = client.get(
"/api/admin/audit",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_admin_required_for_user_list(self, client: TestClient):
unique = str(uuid.uuid4())[:8]
client.post(
"/api/auth/register",
json={
"username": f"regular_{unique}",
"email": f"regular_{unique}@example.com",
"password": "testpass123",
},
)
resp = client.post(
"/api/auth/login",
data={"username": f"regular_{unique}", "password": "testpass123"},
)
token = resp.json()["access_token"]
response = client.get(
"/api/admin/users",
headers={"Authorization": f"Bearer {token}"},
)
assert response.status_code == 403
def test_unauthenticated_admin_access(self, client: TestClient):
response = client.get("/api/admin/users")
assert response.status_code == 401