""" 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)