123
This commit is contained in:
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
BIN
backend/tests/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
backend/tests/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
backend/tests/__pycache__/conftest.cpython-311-pytest-8.3.3.pyc
Normal file
BIN
backend/tests/__pycache__/conftest.cpython-311-pytest-8.3.3.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
86
backend/tests/conftest.py
Normal file
86
backend/tests/conftest.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
Shared test fixtures for Weibo-HotSign backend tests.
|
||||
|
||||
Uses SQLite in-memory for database tests and a simple dict-based
|
||||
fake Redis for refresh-token tests, so no external services are needed.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Ensure backend/ is on sys.path so `shared` and `app` imports work
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# --------------- override shared settings BEFORE any app import ---------------
|
||||
os.environ["DATABASE_URL"] = "sqlite+aiosqlite://"
|
||||
os.environ["REDIS_URL"] = "redis://localhost:6379/0"
|
||||
os.environ["JWT_SECRET_KEY"] = "test-secret-key"
|
||||
os.environ["COOKIE_ENCRYPTION_KEY"] = "test-cookie-key"
|
||||
|
||||
# Create the test engine BEFORE importing shared.models so we can swap it in
|
||||
TEST_ENGINE = create_async_engine("sqlite+aiosqlite://", echo=False)
|
||||
TestSessionLocal = sessionmaker(TEST_ENGINE, class_=AsyncSession, expire_on_commit=False)
|
||||
|
||||
# Now patch shared.models.base module-level objects before they get used
|
||||
import shared.models.base as _base_mod # noqa: E402
|
||||
|
||||
_base_mod.engine = TEST_ENGINE
|
||||
_base_mod.AsyncSessionLocal = TestSessionLocal
|
||||
|
||||
from shared.models.base import Base # noqa: E402
|
||||
from shared.models import User # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create a single event loop for the whole test session."""
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(autouse=True)
|
||||
async def setup_db():
|
||||
"""Create all tables before each test, drop after."""
|
||||
async with TEST_ENGINE.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield
|
||||
async with TEST_ENGINE.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def db_session() -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Yield a fresh async DB session."""
|
||||
async with TestSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
|
||||
# --------------- Fake Redis for refresh-token tests ---------------
|
||||
|
||||
class FakeRedis:
|
||||
"""Minimal async Redis stand-in backed by a plain dict."""
|
||||
|
||||
def __init__(self):
|
||||
self._store: dict[str, str] = {}
|
||||
|
||||
async def setex(self, key: str, ttl: int, value: str):
|
||||
self._store[key] = value
|
||||
|
||||
async def get(self, key: str):
|
||||
return self._store.get(key)
|
||||
|
||||
async def delete(self, key: str):
|
||||
self._store.pop(key, None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_redis():
|
||||
return FakeRedis()
|
||||
214
backend/tests/test_api_accounts.py
Normal file
214
backend/tests/test_api_accounts.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Tests for api_service account CRUD endpoints.
|
||||
Validates tasks 4.1 and 4.2.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import patch
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from shared.models import get_db
|
||||
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,
|
||||
):
|
||||
# We need both clients: auth for getting tokens, api for account ops
|
||||
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"acctuser{suffix}",
|
||||
"email": f"acct{suffix}@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
assert reg.status_code == 201, f"Register failed: {reg.json()}"
|
||||
resp = await auth_client.post("/auth/login", json={
|
||||
"email": f"acct{suffix}@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
login_body = resp.json()
|
||||
assert resp.status_code == 200, f"Login failed: {login_body}"
|
||||
# Handle both wrapped (success_response) and unwrapped token formats
|
||||
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}"}
|
||||
|
||||
|
||||
# ===================== Basic structure tests =====================
|
||||
|
||||
|
||||
class TestAPIServiceBase:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health(self, client):
|
||||
_, api = client
|
||||
resp = await api.get("/health")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["success"] is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_root(self, client):
|
||||
_, api = client
|
||||
resp = await api.get("/")
|
||||
assert resp.status_code == 200
|
||||
assert "API Service" in resp.json()["data"]["service"]
|
||||
|
||||
|
||||
# ===================== Account CRUD tests =====================
|
||||
|
||||
|
||||
class TestAccountCRUD:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_account(self, client):
|
||||
auth, api = client
|
||||
token = await _register_and_login(auth)
|
||||
|
||||
resp = await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": "12345",
|
||||
"cookie": "SUB=abc; SUBP=xyz;",
|
||||
"remark": "test account",
|
||||
}, headers=_auth_header(token))
|
||||
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert body["data"]["weibo_user_id"] == "12345"
|
||||
assert body["data"]["status"] == "pending"
|
||||
assert body["data"]["remark"] == "test account"
|
||||
# Cookie plaintext must NOT appear in response
|
||||
assert "SUB=abc" not in str(body)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_accounts(self, client):
|
||||
auth, api = client
|
||||
token = await _register_and_login(auth, "list")
|
||||
|
||||
# Create two accounts
|
||||
for i in range(2):
|
||||
await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": f"uid{i}",
|
||||
"cookie": f"cookie{i}",
|
||||
}, headers=_auth_header(token))
|
||||
|
||||
resp = await api.get("/api/v1/accounts", headers=_auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_account_detail(self, client):
|
||||
auth, api = client
|
||||
token = await _register_and_login(auth, "detail")
|
||||
|
||||
create_resp = await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": "99",
|
||||
"cookie": "c=1",
|
||||
"remark": "my remark",
|
||||
}, headers=_auth_header(token))
|
||||
account_id = create_resp.json()["data"]["id"]
|
||||
|
||||
resp = await api.get(f"/api/v1/accounts/{account_id}", headers=_auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"]["remark"] == "my remark"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_account_remark(self, client):
|
||||
auth, api = client
|
||||
token = await _register_and_login(auth, "upd")
|
||||
|
||||
create_resp = await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": "55",
|
||||
"cookie": "c=old",
|
||||
}, headers=_auth_header(token))
|
||||
account_id = create_resp.json()["data"]["id"]
|
||||
|
||||
resp = await api.put(f"/api/v1/accounts/{account_id}", json={
|
||||
"remark": "updated remark",
|
||||
}, headers=_auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"]["remark"] == "updated remark"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_account(self, client):
|
||||
auth, api = client
|
||||
token = await _register_and_login(auth, "del")
|
||||
|
||||
create_resp = await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": "77",
|
||||
"cookie": "c=del",
|
||||
}, headers=_auth_header(token))
|
||||
account_id = create_resp.json()["data"]["id"]
|
||||
|
||||
resp = await api.delete(f"/api/v1/accounts/{account_id}", headers=_auth_header(token))
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Verify it's gone
|
||||
resp2 = await api.get(f"/api/v1/accounts/{account_id}", headers=_auth_header(token))
|
||||
assert resp2.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_other_users_account_forbidden(self, client):
|
||||
auth, api = client
|
||||
token_a = await _register_and_login(auth, "ownerA")
|
||||
token_b = await _register_and_login(auth, "ownerB")
|
||||
|
||||
# User A creates an account
|
||||
create_resp = await api.post("/api/v1/accounts", json={
|
||||
"weibo_user_id": "111",
|
||||
"cookie": "c=a",
|
||||
}, headers=_auth_header(token_a))
|
||||
account_id = create_resp.json()["data"]["id"]
|
||||
|
||||
# User B tries to access it
|
||||
resp = await api.get(f"/api/v1/accounts/{account_id}", headers=_auth_header(token_b))
|
||||
assert resp.status_code == 403
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unauthenticated_request_rejected(self, client):
|
||||
_, api = client
|
||||
resp = await api.get("/api/v1/accounts")
|
||||
assert resp.status_code in (401, 403)
|
||||
238
backend/tests/test_api_signin_logs.py
Normal file
238
backend/tests/test_api_signin_logs.py
Normal 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)
|
||||
226
backend/tests/test_api_tasks.py
Normal file
226
backend/tests/test_api_tasks.py
Normal file
@@ -0,0 +1,226 @@
|
||||
"""
|
||||
Tests for API_Service task management endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from shared.models import User, Account, Task
|
||||
from auth_service.app.utils.security import create_access_token
|
||||
from shared.crypto import encrypt_cookie, derive_key
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_user(db_session: AsyncSession) -> User:
|
||||
"""Create a test user."""
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
hashed_password="hashed_password",
|
||||
)
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def test_account(db_session: AsyncSession, test_user: User) -> Account:
|
||||
"""Create a test account."""
|
||||
key = derive_key("test-cookie-key")
|
||||
ciphertext, iv = encrypt_cookie("test_cookie_data", key)
|
||||
|
||||
account = Account(
|
||||
user_id=test_user.id,
|
||||
weibo_user_id="123456",
|
||||
remark="Test Account",
|
||||
encrypted_cookies=ciphertext,
|
||||
iv=iv,
|
||||
status="pending",
|
||||
)
|
||||
db_session.add(account)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(account)
|
||||
return account
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth_headers(test_user: User) -> dict:
|
||||
"""Generate JWT auth headers for test user."""
|
||||
token = create_access_token({"sub": test_user.id})
|
||||
return {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_valid_cron(
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_account: Account,
|
||||
auth_headers: dict,
|
||||
):
|
||||
"""Test creating a task with valid cron expression."""
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
f"/api/v1/accounts/{test_account.id}/tasks",
|
||||
json={"cron_expression": "0 9 * * *"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["cron_expression"] == "0 9 * * *"
|
||||
assert data["data"]["is_enabled"] is True
|
||||
assert data["data"]["account_id"] == test_account.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_invalid_cron(
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_account: Account,
|
||||
auth_headers: dict,
|
||||
):
|
||||
"""Test creating a task with invalid cron expression."""
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.post(
|
||||
f"/api/v1/accounts/{test_account.id}/tasks",
|
||||
json={"cron_expression": "invalid cron"},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_tasks(
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_account: Account,
|
||||
auth_headers: dict,
|
||||
):
|
||||
"""Test listing tasks for an account."""
|
||||
# Create two tasks
|
||||
task1 = Task(account_id=test_account.id, cron_expression="0 9 * * *", is_enabled=True)
|
||||
task2 = Task(account_id=test_account.id, cron_expression="0 18 * * *", is_enabled=False)
|
||||
db_session.add_all([task1, task2])
|
||||
await db_session.commit()
|
||||
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.get(
|
||||
f"/api/v1/accounts/{test_account.id}/tasks",
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_task(
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_account: Account,
|
||||
auth_headers: dict,
|
||||
):
|
||||
"""Test updating a task (enable/disable)."""
|
||||
task = Task(account_id=test_account.id, cron_expression="0 9 * * *", is_enabled=True)
|
||||
db_session.add(task)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(task)
|
||||
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.put(
|
||||
f"/api/v1/tasks/{task.id}",
|
||||
json={"is_enabled": False},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
assert data["data"]["is_enabled"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_task(
|
||||
db_session: AsyncSession,
|
||||
test_user: User,
|
||||
test_account: Account,
|
||||
auth_headers: dict,
|
||||
):
|
||||
"""Test deleting a task."""
|
||||
task = Task(account_id=test_account.id, cron_expression="0 9 * * *", is_enabled=True)
|
||||
db_session.add(task)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(task)
|
||||
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.delete(
|
||||
f"/api/v1/tasks/{task.id}",
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
# Verify task is deleted
|
||||
from sqlalchemy import select
|
||||
result = await db_session.execute(select(Task).where(Task.id == task.id))
|
||||
deleted_task = result.scalar_one_or_none()
|
||||
assert deleted_task is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_other_user_task_forbidden(
|
||||
db_session: AsyncSession,
|
||||
test_account: Account,
|
||||
):
|
||||
"""Test that users cannot access tasks from other users' accounts."""
|
||||
# Create another user
|
||||
other_user = User(
|
||||
username="otheruser",
|
||||
email="other@example.com",
|
||||
hashed_password="hashed_password",
|
||||
)
|
||||
db_session.add(other_user)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(other_user)
|
||||
|
||||
# Create a task for test_account
|
||||
task = Task(account_id=test_account.id, cron_expression="0 9 * * *", is_enabled=True)
|
||||
db_session.add(task)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(task)
|
||||
|
||||
# Try to access with other_user's token
|
||||
other_token = create_access_token({"sub": other_user.id})
|
||||
other_headers = {"Authorization": f"Bearer {other_token}"}
|
||||
|
||||
from api_service.app.main import app
|
||||
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
response = await client.put(
|
||||
f"/api/v1/tasks/{task.id}",
|
||||
json={"is_enabled": False},
|
||||
headers=other_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
317
backend/tests/test_auth_service.py
Normal file
317
backend/tests/test_auth_service.py
Normal file
@@ -0,0 +1,317 @@
|
||||
"""
|
||||
Tests for auth_service: security utils, AuthService logic, and API endpoints.
|
||||
Validates tasks 2.1 – 2.3.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from unittest.mock import patch, AsyncMock
|
||||
from fastapi import HTTPException
|
||||
|
||||
from shared.models import User
|
||||
from tests.conftest import TestSessionLocal, FakeRedis
|
||||
|
||||
# Import security utilities
|
||||
from auth_service.app.utils.security import (
|
||||
hash_password,
|
||||
verify_password,
|
||||
validate_password_strength,
|
||||
create_access_token,
|
||||
decode_access_token,
|
||||
)
|
||||
from auth_service.app.services.auth_service import AuthService
|
||||
from auth_service.app.schemas.user import UserCreate, UserLogin
|
||||
|
||||
|
||||
# ===================== Password utilities =====================
|
||||
|
||||
|
||||
class TestPasswordUtils:
|
||||
|
||||
def test_hash_and_verify(self):
|
||||
raw = "MyP@ssw0rd"
|
||||
hashed = hash_password(raw)
|
||||
assert verify_password(raw, hashed)
|
||||
|
||||
def test_wrong_password_rejected(self):
|
||||
hashed = hash_password("Correct1!")
|
||||
assert not verify_password("Wrong1!", hashed)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pwd, expected_valid",
|
||||
[
|
||||
("Ab1!abcd", True), # meets all criteria
|
||||
("short1A!", True), # 8 chars, has upper/lower/digit/special – valid
|
||||
("alllower1!", False), # no uppercase
|
||||
("ALLUPPER1!", False), # no lowercase
|
||||
("NoDigits!Aa", False), # no digit
|
||||
("NoSpecial1a", False), # no special char
|
||||
],
|
||||
)
|
||||
def test_password_strength(self, pwd, expected_valid):
|
||||
is_valid, _ = validate_password_strength(pwd)
|
||||
assert is_valid == expected_valid
|
||||
|
||||
def test_password_too_short(self):
|
||||
is_valid, msg = validate_password_strength("Ab1!")
|
||||
assert not is_valid
|
||||
assert "8 characters" in msg
|
||||
|
||||
|
||||
# ===================== JWT utilities =====================
|
||||
|
||||
|
||||
class TestJWT:
|
||||
|
||||
def test_create_and_decode(self):
|
||||
token = create_access_token({"sub": "user-123", "username": "alice"})
|
||||
payload = decode_access_token(token)
|
||||
assert payload is not None
|
||||
assert payload["sub"] == "user-123"
|
||||
|
||||
def test_invalid_token_returns_none(self):
|
||||
assert decode_access_token("not.a.valid.token") is None
|
||||
|
||||
|
||||
# ===================== Refresh token helpers (with fake Redis) =====================
|
||||
|
||||
|
||||
class TestRefreshToken:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_verify_revoke(self, fake_redis):
|
||||
"""Full lifecycle: create → verify → revoke → verify again returns None."""
|
||||
|
||||
async def _fake_get_redis():
|
||||
return fake_redis
|
||||
|
||||
with patch(
|
||||
"auth_service.app.utils.security.get_redis",
|
||||
new=_fake_get_redis,
|
||||
):
|
||||
from auth_service.app.utils.security import (
|
||||
create_refresh_token,
|
||||
verify_refresh_token,
|
||||
revoke_refresh_token,
|
||||
)
|
||||
|
||||
token = await create_refresh_token("user-42")
|
||||
assert isinstance(token, str) and len(token) > 0
|
||||
|
||||
uid = await verify_refresh_token(token)
|
||||
assert uid == "user-42"
|
||||
|
||||
await revoke_refresh_token(token)
|
||||
assert await verify_refresh_token(token) is None
|
||||
|
||||
|
||||
# ===================== AuthService business logic =====================
|
||||
|
||||
|
||||
class TestAuthServiceLogic:
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth_svc(self, db_session):
|
||||
return AuthService(db_session)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_success(self, auth_svc, db_session):
|
||||
data = UserCreate(username="newuser", email="new@example.com", password="Str0ng!Pass")
|
||||
user = await auth_svc.create_user(data)
|
||||
assert user.username == "newuser"
|
||||
assert user.email == "new@example.com"
|
||||
assert user.hashed_password != "Str0ng!Pass"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user_weak_password_rejected(self, auth_svc):
|
||||
# Use a password that passes Pydantic min_length=8 but fails strength check
|
||||
data = UserCreate(username="weakuser", email="weak@example.com", password="weakpassword")
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
await auth_svc.create_user(data)
|
||||
assert exc_info.value.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_by_email(self, auth_svc, db_session):
|
||||
data = UserCreate(username="findme", email="find@example.com", password="Str0ng!Pass")
|
||||
await auth_svc.create_user(data)
|
||||
found = await auth_svc.get_user_by_email("find@example.com")
|
||||
assert found is not None
|
||||
assert found.username == "findme"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_user_exists(self, auth_svc, db_session):
|
||||
data = UserCreate(username="exists", email="exists@example.com", password="Str0ng!Pass")
|
||||
await auth_svc.create_user(data)
|
||||
email_u, username_u = await auth_svc.check_user_exists("exists@example.com", "other")
|
||||
assert email_u is not None
|
||||
assert username_u is None
|
||||
|
||||
|
||||
# ===================== Auth API endpoint tests =====================
|
||||
|
||||
|
||||
class TestAuthAPI:
|
||||
"""Integration tests hitting the FastAPI app via httpx."""
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def client(self, fake_redis):
|
||||
"""
|
||||
Provide an httpx AsyncClient wired to the auth_service app,
|
||||
with DB session overridden to use the test SQLite engine.
|
||||
"""
|
||||
from shared.models import get_db
|
||||
from auth_service.app.main import app
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
from tests.conftest import TEST_ENGINE, TestSessionLocal, Base
|
||||
|
||||
async with TEST_ENGINE.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
async def override_get_db():
|
||||
async with TestSessionLocal() as session:
|
||||
yield session
|
||||
|
||||
async def _fake_get_redis():
|
||||
return fake_redis
|
||||
|
||||
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=app), base_url="http://test"
|
||||
) as ac:
|
||||
yield ac
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_and_login(self, client):
|
||||
# Register
|
||||
resp = await client.post("/auth/register", json={
|
||||
"username": "apiuser",
|
||||
"email": "api@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
assert resp.status_code == 201
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert body["data"]["username"] == "apiuser"
|
||||
|
||||
# Login
|
||||
resp = await client.post("/auth/login", json={
|
||||
"email": "api@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert "access_token" in body["data"]
|
||||
assert "refresh_token" in body["data"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_wrong_password(self, client):
|
||||
await client.post("/auth/register", json={
|
||||
"username": "wrongpw",
|
||||
"email": "wrongpw@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
resp = await client.post("/auth/login", json={
|
||||
"email": "wrongpw@example.com",
|
||||
"password": "WrongPassword1!",
|
||||
})
|
||||
assert resp.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_duplicate_email(self, client):
|
||||
await client.post("/auth/register", json={
|
||||
"username": "dup1",
|
||||
"email": "dup@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
resp = await client.post("/auth/register", json={
|
||||
"username": "dup2",
|
||||
"email": "dup@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
assert resp.status_code == 409
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_weak_password(self, client):
|
||||
resp = await client.post("/auth/register", json={
|
||||
"username": "weakpwd",
|
||||
"email": "weakpwd@example.com",
|
||||
"password": "weakpassword",
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_me_endpoint(self, client):
|
||||
await client.post("/auth/register", json={
|
||||
"username": "meuser",
|
||||
"email": "me@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
login_resp = await client.post("/auth/login", json={
|
||||
"email": "me@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
token = login_resp.json()["data"]["access_token"]
|
||||
|
||||
resp = await client.get(
|
||||
"/auth/me",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["data"]["username"] == "meuser"
|
||||
assert body["data"]["email"] == "me@example.com"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_endpoint(self, client):
|
||||
await client.post("/auth/register", json={
|
||||
"username": "refreshuser",
|
||||
"email": "refresh@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
login_resp = await client.post("/auth/login", json={
|
||||
"email": "refresh@example.com",
|
||||
"password": "Str0ng!Pass1",
|
||||
})
|
||||
refresh_token = login_resp.json()["data"]["refresh_token"]
|
||||
|
||||
# Refresh
|
||||
resp = await client.post("/auth/refresh", json={
|
||||
"refresh_token": refresh_token,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["success"] is True
|
||||
assert "access_token" in body["data"]
|
||||
new_refresh = body["data"]["refresh_token"]
|
||||
assert new_refresh != refresh_token # rotation
|
||||
|
||||
# Old token should be revoked
|
||||
resp2 = await client.post("/auth/refresh", json={
|
||||
"refresh_token": refresh_token,
|
||||
})
|
||||
assert resp2.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_me_without_token(self, client):
|
||||
resp = await client.get("/auth/me")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unified_error_format(self, client):
|
||||
"""Verify error responses follow the unified format."""
|
||||
resp = await client.post("/auth/login", json={
|
||||
"email": "nobody@example.com",
|
||||
"password": "Whatever1!",
|
||||
})
|
||||
body = resp.json()
|
||||
assert body["success"] is False
|
||||
assert body["data"] is None
|
||||
assert "error" in body
|
||||
171
backend/tests/test_shared.py
Normal file
171
backend/tests/test_shared.py
Normal file
@@ -0,0 +1,171 @@
|
||||
"""
|
||||
Tests for the shared module: crypto, response format, and ORM models.
|
||||
Validates tasks 1.1 – 1.5 (excluding optional PBT task 1.4).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
|
||||
from shared.crypto import derive_key, encrypt_cookie, decrypt_cookie
|
||||
from shared.response import success_response, error_response
|
||||
from shared.models import User, Account, Task, SigninLog
|
||||
|
||||
from tests.conftest import TestSessionLocal
|
||||
|
||||
|
||||
# ===================== Crypto tests =====================
|
||||
|
||||
|
||||
class TestCrypto:
|
||||
"""Verify AES-256-GCM encrypt/decrypt round-trip and error handling."""
|
||||
|
||||
def setup_method(self):
|
||||
self.key = derive_key("test-encryption-key")
|
||||
|
||||
def test_encrypt_decrypt_roundtrip(self):
|
||||
original = "SUB=abc123; SUBP=xyz789;"
|
||||
ct, iv = encrypt_cookie(original, self.key)
|
||||
assert decrypt_cookie(ct, iv, self.key) == original
|
||||
|
||||
def test_different_plaintexts_produce_different_ciphertexts(self):
|
||||
ct1, _ = encrypt_cookie("cookie_a", self.key)
|
||||
ct2, _ = encrypt_cookie("cookie_b", self.key)
|
||||
assert ct1 != ct2
|
||||
|
||||
def test_wrong_key_raises(self):
|
||||
ct, iv = encrypt_cookie("secret", self.key)
|
||||
wrong_key = derive_key("wrong-key")
|
||||
with pytest.raises(Exception):
|
||||
decrypt_cookie(ct, iv, wrong_key)
|
||||
|
||||
def test_empty_string_roundtrip(self):
|
||||
ct, iv = encrypt_cookie("", self.key)
|
||||
assert decrypt_cookie(ct, iv, self.key) == ""
|
||||
|
||||
def test_unicode_roundtrip(self):
|
||||
original = "微博Cookie=值; 中文=测试"
|
||||
ct, iv = encrypt_cookie(original, self.key)
|
||||
assert decrypt_cookie(ct, iv, self.key) == original
|
||||
|
||||
|
||||
# ===================== Response format tests =====================
|
||||
|
||||
|
||||
class TestResponseFormat:
|
||||
"""Verify unified response helpers."""
|
||||
|
||||
def test_success_response_structure(self):
|
||||
resp = success_response({"id": 1}, "ok")
|
||||
assert resp["success"] is True
|
||||
assert resp["data"] == {"id": 1}
|
||||
assert resp["message"] == "ok"
|
||||
|
||||
def test_success_response_defaults(self):
|
||||
resp = success_response()
|
||||
assert resp["success"] is True
|
||||
assert resp["data"] is None
|
||||
assert "Operation successful" in resp["message"]
|
||||
|
||||
def test_error_response_structure(self):
|
||||
resp = error_response("bad", "VALIDATION_ERROR", [{"field": "email"}], 400)
|
||||
assert resp.status_code == 400
|
||||
import json
|
||||
body = json.loads(resp.body)
|
||||
assert body["success"] is False
|
||||
assert body["data"] is None
|
||||
assert body["error"]["code"] == "VALIDATION_ERROR"
|
||||
assert len(body["error"]["details"]) == 1
|
||||
|
||||
|
||||
# ===================== ORM model smoke tests =====================
|
||||
|
||||
|
||||
class TestORMModels:
|
||||
"""Verify ORM models can be created and queried with SQLite."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user(self, db_session):
|
||||
user = User(
|
||||
username="testuser",
|
||||
email="test@example.com",
|
||||
hashed_password="hashed",
|
||||
)
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
|
||||
result = await db_session.execute(select(User).where(User.username == "testuser"))
|
||||
fetched = result.scalar_one()
|
||||
assert fetched.email == "test@example.com"
|
||||
assert fetched.is_active is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_account_linked_to_user(self, db_session):
|
||||
user = User(username="u1", email="u1@x.com", hashed_password="h")
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
|
||||
acct = Account(
|
||||
user_id=user.id,
|
||||
weibo_user_id="12345",
|
||||
remark="test",
|
||||
encrypted_cookies="enc",
|
||||
iv="iv123",
|
||||
)
|
||||
db_session.add(acct)
|
||||
await db_session.commit()
|
||||
|
||||
result = await db_session.execute(select(Account).where(Account.user_id == user.id))
|
||||
fetched = result.scalar_one()
|
||||
assert fetched.weibo_user_id == "12345"
|
||||
assert fetched.status == "pending"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_task_linked_to_account(self, db_session):
|
||||
user = User(username="u2", email="u2@x.com", hashed_password="h")
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
|
||||
acct = Account(
|
||||
user_id=user.id, weibo_user_id="99", remark="r",
|
||||
encrypted_cookies="e", iv="i",
|
||||
)
|
||||
db_session.add(acct)
|
||||
await db_session.commit()
|
||||
|
||||
task = Task(account_id=acct.id, cron_expression="0 8 * * *")
|
||||
db_session.add(task)
|
||||
await db_session.commit()
|
||||
|
||||
result = await db_session.execute(select(Task).where(Task.account_id == acct.id))
|
||||
fetched = result.scalar_one()
|
||||
assert fetched.cron_expression == "0 8 * * *"
|
||||
assert fetched.is_enabled is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_signin_log(self, db_session):
|
||||
user = User(username="u3", email="u3@x.com", hashed_password="h")
|
||||
db_session.add(user)
|
||||
await db_session.commit()
|
||||
|
||||
acct = Account(
|
||||
user_id=user.id, weibo_user_id="77", remark="r",
|
||||
encrypted_cookies="e", iv="i",
|
||||
)
|
||||
db_session.add(acct)
|
||||
await db_session.commit()
|
||||
|
||||
log = SigninLog(
|
||||
account_id=acct.id,
|
||||
topic_title="超话A",
|
||||
status="success",
|
||||
)
|
||||
db_session.add(log)
|
||||
await db_session.commit()
|
||||
|
||||
result = await db_session.execute(
|
||||
select(SigninLog).where(SigninLog.account_id == acct.id)
|
||||
)
|
||||
fetched = result.scalar_one()
|
||||
assert fetched.status == "success"
|
||||
assert fetched.topic_title == "超话A"
|
||||
Reference in New Issue
Block a user