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:
@@ -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
|
||||
|
||||
223
LinkSyncServer/tests/test_admin.py
Normal file
223
LinkSyncServer/tests/test_admin.py
Normal 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
|
||||
95
LinkSyncServer/tests/test_auth_extended.py
Normal file
95
LinkSyncServer/tests/test_auth_extended.py
Normal 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
|
||||
93
LinkSyncServer/tests/test_sync.py
Normal file
93
LinkSyncServer/tests/test_sync.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
LinkSyncServer - Sync API Tests
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestSync:
|
||||
def test_sync_list_collections(self, client: TestClient):
|
||||
response = client.get("/api/sync/collections")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_sync_get_collection_not_found(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get(
|
||||
"/api/sync/collections/00000000-0000-0000-0000-000000000000",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_sync_create_collection_and_get(self, client: TestClient, auth_headers: dict):
|
||||
col_name = f"Sync Get Test {uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/collections/",
|
||||
json={
|
||||
"name": col_name,
|
||||
"description": "Test",
|
||||
"query_type": "static",
|
||||
"is_public": False,
|
||||
"link_ids": [],
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
collection_id = create_resp.json()["id"]
|
||||
response = client.get(
|
||||
f"/api/sync/collections/{collection_id}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == col_name
|
||||
|
||||
def test_sync_add_links(self, client: TestClient, auth_headers: dict, sample_bookmark_data: dict):
|
||||
bm_data = sample_bookmark_data.copy()
|
||||
bm_data["url"] = f"https://sync-test-{uuid.uuid4().hex[:8]}.com"
|
||||
bm_data["title"] = f"Sync Test {uuid.uuid4().hex[:8]}"
|
||||
bm_resp = client.post("/api/links/", json=bm_data, headers=auth_headers)
|
||||
bookmark_id = bm_resp.json()["id"]
|
||||
|
||||
col_name = f"Sync Add Links {uuid.uuid4().hex[:8]}"
|
||||
col_resp = client.post(
|
||||
"/api/collections/",
|
||||
json={
|
||||
"name": col_name,
|
||||
"query_type": "static",
|
||||
"is_public": False,
|
||||
"link_ids": [],
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
collection_id = col_resp.json()["id"]
|
||||
|
||||
response = client.post(
|
||||
f"/api/sync/collections/{collection_id}/add-links",
|
||||
json=[bookmark_id],
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_sync_delete_collection(self, client: TestClient, auth_headers: dict):
|
||||
col_name = f"Sync Delete {uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/collections/",
|
||||
json={
|
||||
"name": col_name,
|
||||
"query_type": "static",
|
||||
"is_public": False,
|
||||
"link_ids": [],
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
collection_id = create_resp.json()["id"]
|
||||
|
||||
response = client.delete(
|
||||
f"/api/sync/collections/{collection_id}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_sync_list_collections_no_auth(self, client: TestClient):
|
||||
response = client.get("/api/sync/collections")
|
||||
assert response.status_code == 200
|
||||
118
LinkSyncServer/tests/test_tags.py
Normal file
118
LinkSyncServer/tests/test_tags.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""
|
||||
LinkSyncServer - Tags API Tests
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
class TestTags:
|
||||
def test_list_tags_empty(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get("/api/tags/", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_create_tag(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"test-tag-{uuid.uuid4().hex[:8]}"
|
||||
response = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name, "color": "#ff0000", "description": "A test tag"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["name"] == tag_name
|
||||
|
||||
def test_create_tag_duplicate(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"dup-tag-{uuid.uuid4().hex[:8]}"
|
||||
client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name},
|
||||
headers=auth_headers,
|
||||
)
|
||||
response = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_list_tags(self, client: TestClient, auth_headers: dict):
|
||||
client.post("/api/tags/", json={"name": f"list-tag-1-{uuid.uuid4().hex[:8]}"}, headers=auth_headers)
|
||||
client.post("/api/tags/", json={"name": f"list-tag-2-{uuid.uuid4().hex[:8]}"}, headers=auth_headers)
|
||||
response = client.get("/api/tags/", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) >= 2
|
||||
|
||||
def test_get_tag(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"get-tag-{uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name},
|
||||
headers=auth_headers,
|
||||
)
|
||||
tag_id = create_resp.json()["id"]
|
||||
response = client.get(f"/api/tags/{tag_id}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == tag_name
|
||||
|
||||
def test_get_tag_not_found(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get("/api/tags/00000000-0000-0000-0000-000000000000", headers=auth_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_get_tag_by_name(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"find-by-name-{uuid.uuid4().hex[:8]}"
|
||||
client.post("/api/tags/", json={"name": tag_name}, headers=auth_headers)
|
||||
response = client.get(f"/api/tags/name/{tag_name}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == tag_name
|
||||
|
||||
def test_get_tag_by_name_not_found(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get("/api/tags/name/nonexistent-tag-xyz", headers=auth_headers)
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_tag(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"update-tag-{uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name, "color": "#000000"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
tag_id = create_resp.json()["id"]
|
||||
response = client.put(
|
||||
f"/api/tags/{tag_id}",
|
||||
json={"color": "#ffffff", "description": "Updated"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["color"] == "#ffffff"
|
||||
|
||||
def test_delete_tag(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"delete-tag-{uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name},
|
||||
headers=auth_headers,
|
||||
)
|
||||
tag_id = create_resp.json()["id"]
|
||||
response = client.delete(f"/api/tags/{tag_id}", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_tag_links(self, client: TestClient, auth_headers: dict):
|
||||
tag_name = f"links-tag-{uuid.uuid4().hex[:8]}"
|
||||
create_resp = client.post(
|
||||
"/api/tags/",
|
||||
json={"name": tag_name},
|
||||
headers=auth_headers,
|
||||
)
|
||||
tag_id = create_resp.json()["id"]
|
||||
response = client.get(f"/api/tags/{tag_id}/links", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
def test_tag_count(self, client: TestClient, auth_headers: dict):
|
||||
response = client.get("/api/tags/count", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "count" in data
|
||||
Reference in New Issue
Block a user