Adds complete super_admin panel with 9 pages and account owner categories page. Backend includes 5 new DB tables, ~25 API endpoints, settings manager with in-memory cache, and 29 integration tests. Frontend includes reusable admin components (DataTable, Pagination, ActionMenu, etc.) with code-split lazy loading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
143 lines
5.0 KiB
Python
143 lines
5.0 KiB
Python
"""Integration tests for admin feature flag endpoints."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
|
|
class TestAdminFeatureFlags:
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_feature_flag(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""Create a feature flag."""
|
|
response = await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={
|
|
"flag_key": "test_feature",
|
|
"display_name": "Test Feature",
|
|
"description": "A test feature flag",
|
|
},
|
|
headers=admin_auth_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["flag_key"] == "test_feature"
|
|
assert data["display_name"] == "Test Feature"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_feature_flags(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""List feature flags."""
|
|
# Create a flag first
|
|
await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "list_test", "display_name": "List Test"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
|
|
response = await client.get("/api/v1/admin/feature-flags", headers=admin_auth_headers)
|
|
assert response.status_code == 200
|
|
flags = response.json()
|
|
assert len(flags) >= 1
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_feature_flag(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""Update a feature flag."""
|
|
create_resp = await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "update_test", "display_name": "Before"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
flag_id = create_resp.json()["id"]
|
|
|
|
response = await client.put(
|
|
f"/api/v1/admin/feature-flags/{flag_id}",
|
|
json={"display_name": "After"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["display_name"] == "After"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_plan_default(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""Update a plan feature default."""
|
|
create_resp = await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "plan_default_test", "display_name": "Plan Default Test"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
flag_id = create_resp.json()["id"]
|
|
|
|
response = await client.put(
|
|
"/api/v1/admin/feature-flags/plan-defaults",
|
|
json={"plan": "free", "flag_id": flag_id, "enabled": True},
|
|
headers=admin_auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Verify in list
|
|
list_resp = await client.get("/api/v1/admin/feature-flags", headers=admin_auth_headers)
|
|
flag = next(f for f in list_resp.json() if f["id"] == flag_id)
|
|
assert any(d["plan"] == "free" and d["enabled"] for d in flag["plan_defaults"])
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_feature_flag_cascades(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""Delete a feature flag cascades to plan defaults."""
|
|
create_resp = await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "delete_test", "display_name": "Delete Test"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
flag_id = create_resp.json()["id"]
|
|
|
|
# Add a plan default
|
|
await client.put(
|
|
"/api/v1/admin/feature-flags/plan-defaults",
|
|
json={"plan": "pro", "flag_id": flag_id, "enabled": True},
|
|
headers=admin_auth_headers,
|
|
)
|
|
|
|
# Delete the flag
|
|
response = await client.delete(
|
|
f"/api/v1/admin/feature-flags/{flag_id}",
|
|
headers=admin_auth_headers,
|
|
)
|
|
assert response.status_code == 204
|
|
|
|
# Verify gone
|
|
list_resp = await client.get("/api/v1/admin/feature-flags", headers=admin_auth_headers)
|
|
assert not any(f["id"] == flag_id for f in list_resp.json())
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_flag_key_fails(
|
|
self, client: AsyncClient, admin_auth_headers: dict
|
|
):
|
|
"""Duplicate flag_key returns 409."""
|
|
await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "dupe_test", "display_name": "First"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
response = await client.post(
|
|
"/api/v1/admin/feature-flags",
|
|
json={"flag_key": "dupe_test", "display_name": "Second"},
|
|
headers=admin_auth_headers,
|
|
)
|
|
assert response.status_code == 409
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_admin_cannot_access(
|
|
self, client: AsyncClient, auth_headers: dict
|
|
):
|
|
"""Non-admin gets 403."""
|
|
response = await client.get("/api/v1/admin/feature-flags", headers=auth_headers)
|
|
assert response.status_code == 403
|