239 lines
8.0 KiB
Python
239 lines
8.0 KiB
Python
|
|
"""
|
||
|
|
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)
|