新增 157 个测试,总计 368 个测试全部通过: - Organizations API: 50 tests (品牌方↔代理商↔达人关系管理 + 搜索 + 权限) - Projects API: 44 tests (CRUD + 分页 + 状态筛选 + 代理商分配 + 权限) - Dashboard API: 17 tests (三端工作台统计 + 角色隔离 + 认证) - Briefs API: 24 tests (CRUD + 权限 + 数据完整性) - Export API: 22 tests (CSV 导出 + UTF-8 BOM + 角色权限 + 格式验证) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
289 lines
11 KiB
Python
289 lines
11 KiB
Python
"""
|
|
Dashboard API comprehensive tests.
|
|
|
|
Tests cover the three dashboard endpoints:
|
|
- GET /api/v1/dashboard/creator (creator role only)
|
|
- GET /api/v1/dashboard/agency (agency role only)
|
|
- GET /api/v1/dashboard/brand (brand role only)
|
|
|
|
Each endpoint returns zero-valued stats for a freshly registered user
|
|
and enforces role-based access (403 for wrong roles, 401 for unauthenticated).
|
|
|
|
Uses the SQLite-backed test client from conftest.py.
|
|
"""
|
|
import uuid
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from app.main import app
|
|
from app.middleware.rate_limit import RateLimitMiddleware
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Constants
|
|
# ---------------------------------------------------------------------------
|
|
API = "/api/v1"
|
|
REGISTER_URL = f"{API}/auth/register"
|
|
DASHBOARD_CREATOR_URL = f"{API}/dashboard/creator"
|
|
DASHBOARD_AGENCY_URL = f"{API}/dashboard/agency"
|
|
DASHBOARD_BRAND_URL = f"{API}/dashboard/brand"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auto-clear rate limiter state before each test
|
|
# ---------------------------------------------------------------------------
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_rate_limiter():
|
|
"""Reset the in-memory rate limiter between tests."""
|
|
mw = app.middleware_stack
|
|
while mw is not None:
|
|
if isinstance(mw, RateLimitMiddleware):
|
|
mw.requests.clear()
|
|
break
|
|
mw = getattr(mw, "app", None)
|
|
yield
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
def _email(prefix: str = "user") -> str:
|
|
return f"{prefix}-{uuid.uuid4().hex[:8]}@test.com"
|
|
|
|
|
|
async def _register(client: AsyncClient, role: str, name: str | None = None):
|
|
"""Register a user via the API and return (access_token, user_data)."""
|
|
email = _email(role)
|
|
resp = await client.post(REGISTER_URL, json={
|
|
"email": email,
|
|
"password": "test123456",
|
|
"name": name or f"Test {role.title()}",
|
|
"role": role,
|
|
})
|
|
assert resp.status_code == 201, f"Registration failed for {role}: {resp.text}"
|
|
data = resp.json()
|
|
return data["access_token"], data["user"]
|
|
|
|
|
|
def _auth(token: str) -> dict:
|
|
"""Return Authorization header dict."""
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
# ===========================================================================
|
|
# Test class: Creator Dashboard
|
|
# ===========================================================================
|
|
|
|
class TestCreatorDashboard:
|
|
"""GET /api/v1/dashboard/creator"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creator_dashboard_happy_path(self, client: AsyncClient):
|
|
"""Creator gets dashboard stats -- all zeros for a freshly registered user."""
|
|
token, user = await _register(client, "creator")
|
|
|
|
resp = await client.get(DASHBOARD_CREATOR_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
assert data["total_tasks"] == 0
|
|
assert data["pending_script"] == 0
|
|
assert data["pending_video"] == 0
|
|
assert data["in_review"] == 0
|
|
assert data["completed"] == 0
|
|
assert data["rejected"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creator_dashboard_response_keys(self, client: AsyncClient):
|
|
"""Creator dashboard response contains all expected keys."""
|
|
token, _ = await _register(client, "creator")
|
|
|
|
resp = await client.get(DASHBOARD_CREATOR_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
expected_keys = {
|
|
"total_tasks", "pending_script", "pending_video",
|
|
"in_review", "completed", "rejected",
|
|
}
|
|
assert expected_keys.issubset(set(data.keys()))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creator_dashboard_forbidden_for_brand(self, client: AsyncClient):
|
|
"""Brand role cannot access creator dashboard -- expects 403."""
|
|
token, _ = await _register(client, "brand")
|
|
|
|
resp = await client.get(DASHBOARD_CREATOR_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creator_dashboard_forbidden_for_agency(self, client: AsyncClient):
|
|
"""Agency role cannot access creator dashboard -- expects 403."""
|
|
token, _ = await _register(client, "agency")
|
|
|
|
resp = await client.get(DASHBOARD_CREATOR_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# ===========================================================================
|
|
# Test class: Agency Dashboard
|
|
# ===========================================================================
|
|
|
|
class TestAgencyDashboard:
|
|
"""GET /api/v1/dashboard/agency"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_happy_path(self, client: AsyncClient):
|
|
"""Agency gets dashboard stats -- all zeros for a freshly registered user."""
|
|
token, user = await _register(client, "agency")
|
|
|
|
resp = await client.get(DASHBOARD_AGENCY_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
assert data["pending_review"]["script"] == 0
|
|
assert data["pending_review"]["video"] == 0
|
|
assert data["pending_appeal"] == 0
|
|
assert data["today_passed"]["script"] == 0
|
|
assert data["today_passed"]["video"] == 0
|
|
assert data["in_progress"]["script"] == 0
|
|
assert data["in_progress"]["video"] == 0
|
|
assert data["total_creators"] == 0
|
|
assert data["total_tasks"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_response_keys(self, client: AsyncClient):
|
|
"""Agency dashboard response contains all expected keys."""
|
|
token, _ = await _register(client, "agency")
|
|
|
|
resp = await client.get(DASHBOARD_AGENCY_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
expected_keys = {
|
|
"pending_review", "pending_appeal", "today_passed",
|
|
"in_progress", "total_creators", "total_tasks",
|
|
}
|
|
assert expected_keys.issubset(set(data.keys()))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_nested_review_counts(self, client: AsyncClient):
|
|
"""Agency dashboard nested ReviewCount objects have correct structure."""
|
|
token, _ = await _register(client, "agency")
|
|
|
|
resp = await client.get(DASHBOARD_AGENCY_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
for key in ("pending_review", "today_passed", "in_progress"):
|
|
assert "script" in data[key], f"Missing 'script' in {key}"
|
|
assert "video" in data[key], f"Missing 'video' in {key}"
|
|
assert isinstance(data[key]["script"], int)
|
|
assert isinstance(data[key]["video"], int)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_forbidden_for_creator(self, client: AsyncClient):
|
|
"""Creator role cannot access agency dashboard -- expects 403."""
|
|
token, _ = await _register(client, "creator")
|
|
|
|
resp = await client.get(DASHBOARD_AGENCY_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_forbidden_for_brand(self, client: AsyncClient):
|
|
"""Brand role cannot access agency dashboard -- expects 403."""
|
|
token, _ = await _register(client, "brand")
|
|
|
|
resp = await client.get(DASHBOARD_AGENCY_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# ===========================================================================
|
|
# Test class: Brand Dashboard
|
|
# ===========================================================================
|
|
|
|
class TestBrandDashboard:
|
|
"""GET /api/v1/dashboard/brand"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_brand_dashboard_happy_path(self, client: AsyncClient):
|
|
"""Brand gets dashboard stats -- all zeros for a freshly registered user."""
|
|
token, user = await _register(client, "brand")
|
|
|
|
resp = await client.get(DASHBOARD_BRAND_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
assert data["total_projects"] == 0
|
|
assert data["active_projects"] == 0
|
|
assert data["pending_review"]["script"] == 0
|
|
assert data["pending_review"]["video"] == 0
|
|
assert data["total_agencies"] == 0
|
|
assert data["total_tasks"] == 0
|
|
assert data["completed_tasks"] == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_brand_dashboard_response_keys(self, client: AsyncClient):
|
|
"""Brand dashboard response contains all expected keys."""
|
|
token, _ = await _register(client, "brand")
|
|
|
|
resp = await client.get(DASHBOARD_BRAND_URL, headers=_auth(token))
|
|
assert resp.status_code == 200
|
|
|
|
data = resp.json()
|
|
expected_keys = {
|
|
"total_projects", "active_projects", "pending_review",
|
|
"total_agencies", "total_tasks", "completed_tasks",
|
|
}
|
|
assert expected_keys.issubset(set(data.keys()))
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_brand_dashboard_forbidden_for_creator(self, client: AsyncClient):
|
|
"""Creator role cannot access brand dashboard -- expects 403."""
|
|
token, _ = await _register(client, "creator")
|
|
|
|
resp = await client.get(DASHBOARD_BRAND_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_brand_dashboard_forbidden_for_agency(self, client: AsyncClient):
|
|
"""Agency role cannot access brand dashboard -- expects 403."""
|
|
token, _ = await _register(client, "agency")
|
|
|
|
resp = await client.get(DASHBOARD_BRAND_URL, headers=_auth(token))
|
|
assert resp.status_code == 403
|
|
|
|
|
|
# ===========================================================================
|
|
# Test class: Dashboard Authentication
|
|
# ===========================================================================
|
|
|
|
class TestDashboardAuth:
|
|
"""Unauthenticated access to all dashboard endpoints."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_creator_dashboard_unauthenticated(self, client: AsyncClient):
|
|
"""Unauthenticated request to creator dashboard returns 401."""
|
|
resp = await client.get(DASHBOARD_CREATOR_URL)
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_agency_dashboard_unauthenticated(self, client: AsyncClient):
|
|
"""Unauthenticated request to agency dashboard returns 401."""
|
|
resp = await client.get(DASHBOARD_AGENCY_URL)
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_brand_dashboard_unauthenticated(self, client: AsyncClient):
|
|
"""Unauthenticated request to brand dashboard returns 401."""
|
|
resp = await client.get(DASHBOARD_BRAND_URL)
|
|
assert resp.status_code == 401
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_dashboard_with_invalid_token(self, client: AsyncClient):
|
|
"""Request with an invalid Bearer token returns 401."""
|
|
headers = {"Authorization": "Bearer invalid-garbage-token"}
|
|
|
|
for url in (DASHBOARD_CREATOR_URL, DASHBOARD_AGENCY_URL, DASHBOARD_BRAND_URL):
|
|
resp = await client.get(url, headers=headers)
|
|
assert resp.status_code == 401, f"Expected 401 for {url}, got {resp.status_code}"
|