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

@@ -1,32 +1,88 @@
"""
LinkSyncServer - Test Configuration
Database lifecycle:
- pytest_configure: Creates fresh test database before any tests run
- pytest_unconfigure: Destroys test database after all tests complete
- Per-test: Transaction rollback ensures test isolation
"""
import os
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
from models.base import Base, get_engine
from models.base import Base
import models.base
SQLALCHEMY_DATABASE_URL = "sqlite:///test_linksync.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
TEST_DATABASE_URL = "sqlite:///test_linksync.db"
def pytest_configure(config):
"""Called before any tests run. Creates fresh test database."""
if os.path.exists("test_linksync.db"):
os.remove("test_linksync.db")
test_engine = create_engine(
TEST_DATABASE_URL,
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(bind=test_engine)
# Override the module-level get_engine to return our test engine
original_get_engine = models.base.get_engine
original_get_session = models.base.get_session
def test_get_engine():
return test_engine
def test_get_session():
Session = sessionmaker(bind=test_engine)
return Session()
models.base.get_engine = test_get_engine
models.base.get_session = test_get_session
# Store originals for cleanup
config._test_engine = test_engine
config._original_get_engine = original_get_engine
config._original_get_session = original_get_session
def pytest_unconfigure(config):
"""Called after all tests complete. Destroys test database."""
# Restore original functions
if hasattr(config, "_original_get_engine"):
models.base.get_engine = config._original_get_engine
models.base.get_session = config._original_get_session
# Drop all tables
if hasattr(config, "_test_engine"):
Base.metadata.drop_all(bind=config._test_engine)
config._test_engine.dispose()
# Remove the test database file
if os.path.exists("test_linksync.db"):
os.remove("test_linksync.db")
@pytest.fixture(scope="session")
def test_engine():
Base.metadata.create_all(bind=engine)
yield engine
Base.metadata.drop_all(bind=engine)
def test_engine(pytestconfig):
"""Provides the test engine for direct database access in tests."""
return pytestconfig._test_engine
@pytest.fixture
def db_session(test_engine):
"""Provides a transactional session that rolls back after each test."""
connection = test_engine.connect()
transaction = connection.begin()
session = TestingSessionLocal(bind=connection)
Session = sessionmaker(bind=connection)
session = Session()
yield session
@@ -37,6 +93,7 @@ def db_session(test_engine):
@pytest.fixture
def client():
"""Provides a TestClient with the test database already configured."""
from app import app
with TestClient(app) as c:
yield c