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,95 @@
"""
LinkSyncServer - Auth API Tests (extended)
"""
import uuid
import pytest
from fastapi.testclient import TestClient
class TestAuthExtended:
def test_list_api_keys(self, client: TestClient, admin_token: str):
response = client.get(
"/api/auth/api-keys",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_list_api_keys_empty(self, client: TestClient, admin_token: str):
response = client.get(
"/api/auth/api-keys",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_and_list_api_key(self, client: TestClient, admin_token: str):
import uuid
key_name = f"test-key-{uuid.uuid4().hex[:8]}"
client.post(
"/api/auth/api-key",
params={"name": key_name},
headers={"Authorization": f"Bearer {admin_token}"},
)
response = client.get(
"/api/auth/api-keys",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
keys = response.json()
assert any(k["name"] == key_name for k in keys)
def test_get_api_key(self, client: TestClient, admin_token: str):
import uuid
key_name = f"get-key-{uuid.uuid4().hex[:8]}"
create_resp = client.post(
"/api/auth/api-key",
params={"name": key_name},
headers={"Authorization": f"Bearer {admin_token}"},
)
key_id = create_resp.json()["key_id"]
response = client.get(
f"/api/auth/api-key/{key_id}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == key_name
def test_get_api_key_not_found(self, client: TestClient, admin_token: str):
response = client.get(
"/api/auth/api-key/00000000-0000-0000-0000-000000000000",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 404
def test_delete_api_key(self, client: TestClient, admin_token: str):
import uuid
key_name = f"del-key-{uuid.uuid4().hex[:8]}"
create_resp = client.post(
"/api/auth/api-key",
params={"name": key_name},
headers={"Authorization": f"Bearer {admin_token}"},
)
key_id = create_resp.json()["key_id"]
response = client.delete(
f"/api/auth/api-key/{key_id}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
def test_delete_api_key_not_found(self, client: TestClient, admin_token: str):
response = client.delete(
"/api/auth/api-key/00000000-0000-0000-0000-000000000000",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 404
def test_api_key_requires_auth(self, client: TestClient):
response = client.get("/api/auth/api-keys")
assert response.status_code == 401
def test_create_api_key_requires_auth(self, client: TestClient):
response = client.post("/api/auth/api-key", params={"name": "test"})
assert response.status_code == 401