This commit is contained in:
2026-03-09 14:05:00 +08:00
commit 754e720ba7
105 changed files with 5890 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
"""
Tests for api_service signin log query endpoints.
Validates task 6.1.
"""
import pytest
import pytest_asyncio
from datetime import datetime, timedelta
from unittest.mock import patch
from httpx import AsyncClient, ASGITransport
from shared.models import get_db, Account, SigninLog
from tests.conftest import TEST_ENGINE, TestSessionLocal, Base, FakeRedis
@pytest_asyncio.fixture
async def client():
"""
Provide an httpx AsyncClient wired to the api_service app,
with DB overridden to test SQLite and a fake Redis for auth tokens.
"""
fake_redis = FakeRedis()
async with TEST_ENGINE.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# Import apps after DB is ready
from api_service.app.main import app as api_app
from auth_service.app.main import app as auth_app
async def override_get_db():
async with TestSessionLocal() as session:
yield session
async def _fake_get_redis():
return fake_redis
api_app.dependency_overrides[get_db] = override_get_db
auth_app.dependency_overrides[get_db] = override_get_db
with patch(
"auth_service.app.utils.security.get_redis",
new=_fake_get_redis,
):
async with AsyncClient(
transport=ASGITransport(app=auth_app), base_url="http://auth"
) as auth_client, AsyncClient(
transport=ASGITransport(app=api_app), base_url="http://api"
) as api_client:
yield auth_client, api_client
api_app.dependency_overrides.clear()
auth_app.dependency_overrides.clear()
async def _register_and_login(auth_client: AsyncClient, suffix: str = "1") -> str:
"""Helper: register a user and return an access token."""
reg = await auth_client.post("/auth/register", json={
"username": f"loguser{suffix}",
"email": f"log{suffix}@example.com",
"password": "Str0ng!Pass1",
})
assert reg.status_code == 201
resp = await auth_client.post("/auth/login", json={
"email": f"log{suffix}@example.com",
"password": "Str0ng!Pass1",
})
login_body = resp.json()
assert resp.status_code == 200
if "data" in login_body:
return login_body["data"]["access_token"]
return login_body["access_token"]
def _auth_header(token: str) -> dict:
return {"Authorization": f"Bearer {token}"}
async def _create_account(api: AsyncClient, token: str, weibo_id: str) -> str:
"""Helper: create an account and return its ID."""
resp = await api.post("/api/v1/accounts", json={
"weibo_user_id": weibo_id,
"cookie": f"cookie_{weibo_id}",
}, headers=_auth_header(token))
assert resp.status_code == 201
return resp.json()["data"]["id"]
async def _create_signin_logs(db, account_id: str, count: int, statuses: list = None):
"""Helper: create signin logs for testing."""
if statuses is None:
statuses = ["success"] * count
base_time = datetime.utcnow()
for i in range(count):
log = SigninLog(
account_id=account_id,
topic_title=f"Topic {i}",
status=statuses[i] if i < len(statuses) else "success",
signed_at=base_time - timedelta(hours=i), # Descending order
)
db.add(log)
await db.commit()
# ===================== Signin Log Query Tests =====================
class TestSigninLogQuery:
@pytest.mark.asyncio
async def test_get_signin_logs_empty(self, client):
"""Test querying logs for an account with no logs."""
auth, api = client
token = await _register_and_login(auth, "empty")
account_id = await _create_account(api, token, "empty_acc")
resp = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs",
headers=_auth_header(token)
)
assert resp.status_code == 200
data = resp.json()["data"]
assert data["total"] == 0
assert len(data["items"]) == 0
@pytest.mark.asyncio
async def test_get_signin_logs_with_data(self, client):
"""Test querying logs returns data in descending order."""
auth, api = client
token = await _register_and_login(auth, "data")
account_id = await _create_account(api, token, "data_acc")
# Create logs directly in DB
async with TestSessionLocal() as db:
await _create_signin_logs(db, account_id, 5)
resp = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs",
headers=_auth_header(token)
)
assert resp.status_code == 200
data = resp.json()["data"]
assert data["total"] == 5
assert len(data["items"]) == 5
# Verify descending order by signed_at
items = data["items"]
for i in range(len(items) - 1):
assert items[i]["signed_at"] >= items[i + 1]["signed_at"]
@pytest.mark.asyncio
async def test_signin_logs_pagination(self, client):
"""Test pagination works correctly."""
auth, api = client
token = await _register_and_login(auth, "page")
account_id = await _create_account(api, token, "page_acc")
# Create 10 logs
async with TestSessionLocal() as db:
await _create_signin_logs(db, account_id, 10)
# Page 1, size 3
resp = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs?page=1&size=3",
headers=_auth_header(token)
)
assert resp.status_code == 200
data = resp.json()["data"]
assert data["total"] == 10
assert len(data["items"]) == 3
assert data["page"] == 1
assert data["size"] == 3
assert data["total_pages"] == 4
# Page 2, size 3
resp2 = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs?page=2&size=3",
headers=_auth_header(token)
)
data2 = resp2.json()["data"]
assert len(data2["items"]) == 3
assert data2["page"] == 2
@pytest.mark.asyncio
async def test_signin_logs_status_filter(self, client):
"""Test status filtering works correctly."""
auth, api = client
token = await _register_and_login(auth, "filter")
account_id = await _create_account(api, token, "filter_acc")
# Create logs with different statuses
async with TestSessionLocal() as db:
statuses = ["success", "success", "failed_network", "success", "failed_already_signed"]
await _create_signin_logs(db, account_id, 5, statuses)
# Filter by success
resp = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs?status=success",
headers=_auth_header(token)
)
assert resp.status_code == 200
data = resp.json()["data"]
assert data["total"] == 3
assert all(item["status"] == "success" for item in data["items"])
# Filter by failed_network
resp2 = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs?status=failed_network",
headers=_auth_header(token)
)
data2 = resp2.json()["data"]
assert data2["total"] == 1
assert data2["items"][0]["status"] == "failed_network"
@pytest.mark.asyncio
async def test_access_other_users_logs_forbidden(self, client):
"""Test that users cannot access other users' signin logs."""
auth, api = client
token_a = await _register_and_login(auth, "logA")
token_b = await _register_and_login(auth, "logB")
# User A creates an account
account_id = await _create_account(api, token_a, "logA_acc")
# User B tries to access logs
resp = await api.get(
f"/api/v1/accounts/{account_id}/signin-logs",
headers=_auth_header(token_b)
)
assert resp.status_code == 403
@pytest.mark.asyncio
async def test_unauthenticated_logs_request_rejected(self, client):
"""Test that unauthenticated requests are rejected."""
_, api = client
resp = await api.get("/api/v1/accounts/fake-id/signin-logs")
assert resp.status_code in (401, 403)