feat: AI 余额监控服务(平台/账号 CRUD、并发轮询、Telegram 阈值提醒)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""API 全链路测试:认证、平台/账号 CRUD、设置。"""
|
||||
|
||||
import base64
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.api import create_app
|
||||
from app.config import Config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(test_db, tmp_path, monkeypatch):
|
||||
from app import config as cfgmod
|
||||
|
||||
monkeypatch.setattr(cfgmod, "CONFIG_PATH", tmp_path / "config.json")
|
||||
cfg = Config(
|
||||
port=8000, password="admin123", global_interval_seconds=60,
|
||||
retry_count=2, timeout_seconds=10, max_workers=4,
|
||||
)
|
||||
app = create_app(cfg)
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
def _auth(client):
|
||||
return {"Authorization": "Bearer " + client.post("/api/login", json={"password": "admin123"}).json()["token"]}
|
||||
|
||||
|
||||
class TestAuth:
|
||||
def test_unauthorized_blocked(self, client):
|
||||
assert client.get("/api/platforms").status_code == 401
|
||||
|
||||
def test_wrong_password(self, client):
|
||||
assert client.post("/api/login", json={"password": "nope"}).status_code == 401
|
||||
|
||||
def test_login_ok(self, client):
|
||||
resp = client.post("/api/login", json={"password": "admin123"})
|
||||
assert resp.status_code == 200 and resp.json()["token"]
|
||||
|
||||
def test_logout_invalidates(self, client):
|
||||
token = _auth(client)["Authorization"].removeprefix("Bearer ")
|
||||
assert client.post("/api/logout", headers={"Authorization": f"Bearer {token}"}).status_code == 200
|
||||
assert client.get("/api/platforms", headers={"Authorization": f"Bearer {token}"}).status_code == 401
|
||||
|
||||
|
||||
PLATFORM_PAYLOAD = {
|
||||
"name": "OpenAI", "currency": "USD", "icon": "OpenAI", "method": "GET",
|
||||
"url": "https://x.test?key={{apiKey}}",
|
||||
"headers": {"Authorization": "Bearer {{apiKey}}"},
|
||||
"body": "", "balance_path": "data.balance",
|
||||
"interval_seconds": 120, "retry_count": 1, "timeout_seconds": 15,
|
||||
"enabled": True, "note": "",
|
||||
}
|
||||
|
||||
|
||||
class TestPlatforms:
|
||||
def test_crud_flow(self, client):
|
||||
h = _auth(client)
|
||||
pid = client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h).json()["id"]
|
||||
|
||||
lst = client.get("/api/platforms", headers=h).json()
|
||||
assert len(lst) == 1 and lst[0]["account_count"] == 0 and lst[0]["url"] == PLATFORM_PAYLOAD["url"]
|
||||
|
||||
upd = client.put(f"/api/platforms/{pid}", json={"currency": "CNY", "interval_seconds": 300}, headers=h)
|
||||
assert upd.status_code == 200
|
||||
assert client.get("/api/platforms", headers=h).json()[0]["currency"] == "CNY"
|
||||
|
||||
assert client.delete(f"/api/platforms/{pid}", headers=h).status_code == 200
|
||||
assert client.get("/api/platforms", headers=h).json() == []
|
||||
|
||||
def test_duplicate_name_409(self, client):
|
||||
h = _auth(client)
|
||||
client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h)
|
||||
assert client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h).status_code == 409
|
||||
|
||||
def test_validation_errors(self, client):
|
||||
h = _auth(client)
|
||||
assert client.post("/api/platforms", json=dict(PLATFORM_PAYLOAD, method="DELETE"), headers=h).status_code == 422
|
||||
assert client.post("/api/platforms", json=dict(PLATFORM_PAYLOAD, url=""), headers=h).status_code == 422
|
||||
|
||||
def test_delete_cascades_accounts(self, client, test_db):
|
||||
from app import db
|
||||
|
||||
h = _auth(client)
|
||||
pid = client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h).json()["id"]
|
||||
client.post("/api/accounts", json={"platform_id": pid, "name": "a1", "api_key": "sk-1"}, headers=h)
|
||||
client.delete(f"/api/platforms/{pid}", headers=h)
|
||||
with db.get_conn() as conn:
|
||||
assert conn.execute("SELECT COUNT(*) FROM accounts").fetchone()[0] == 0
|
||||
|
||||
|
||||
class TestAccounts:
|
||||
def _make_platform(self, client, h):
|
||||
return client.post("/api/platforms", json=PLATFORM_PAYLOAD, headers=h).json()["id"]
|
||||
|
||||
def test_create_returns_decoded_key(self, client):
|
||||
h = _auth(client)
|
||||
pid = self._make_platform(client, h)
|
||||
aid = client.post("/api/accounts", json={"platform_id": pid, "name": "主", "api_key": "sk-secret", "threshold": 10}, headers=h).json()["id"]
|
||||
acc = client.get("/api/accounts", headers=h).json()[0]
|
||||
assert acc["id"] == aid and acc["api_key"] == "sk-secret"
|
||||
assert acc["platform_name"] == "OpenAI" and acc["currency"] == "USD"
|
||||
|
||||
def test_key_stored_base64(self, client, test_db):
|
||||
from app import db
|
||||
|
||||
h = _auth(client)
|
||||
pid = self._make_platform(client, h)
|
||||
client.post("/api/accounts", json={"platform_id": pid, "name": "主", "api_key": "sk-secret"}, headers=h)
|
||||
with db.get_conn() as conn:
|
||||
raw = conn.execute("SELECT api_key FROM accounts").fetchone()[0]
|
||||
assert raw == base64.b64encode(b"sk-secret").decode()
|
||||
|
||||
def test_update_and_delete(self, client):
|
||||
h = _auth(client)
|
||||
pid = self._make_platform(client, h)
|
||||
aid = client.post("/api/accounts", json={"platform_id": pid, "name": "主", "api_key": "sk-1"}, headers=h).json()["id"]
|
||||
assert client.put(f"/api/accounts/{aid}", json={"threshold": 5.5, "name": "新名"}, headers=h).status_code == 200
|
||||
acc = client.get("/api/accounts", headers=h).json()[0]
|
||||
assert acc["name"] == "新名" and acc["threshold"] == 5.5
|
||||
assert client.delete(f"/api/accounts/{aid}", headers=h).status_code == 200
|
||||
assert client.get("/api/accounts", headers=h).json() == []
|
||||
|
||||
def test_account_requires_platform(self, client):
|
||||
h = _auth(client)
|
||||
assert client.post("/api/accounts", json={"platform_id": 999, "name": "x", "api_key": "k"}, headers=h).status_code == 404
|
||||
|
||||
def test_check_now_disabled_account(self, client):
|
||||
h = _auth(client)
|
||||
pid = self._make_platform(client, h)
|
||||
aid = client.post("/api/accounts", json={"platform_id": pid, "name": "x", "api_key": "k", "enabled": False}, headers=h).json()["id"]
|
||||
assert client.post(f"/api/accounts/{aid}/check", headers=h).status_code == 409
|
||||
|
||||
def test_history_flow(self, client, test_db):
|
||||
from app import db
|
||||
|
||||
h = _auth(client)
|
||||
pid = self._make_platform(client, h)
|
||||
aid = client.post("/api/accounts", json={"platform_id": pid, "name": "x", "api_key": "k"}, headers=h).json()["id"]
|
||||
with db.get_conn() as conn:
|
||||
conn.execute("INSERT INTO balance_history (account_id, balance) VALUES (?, ?)", (aid, 1.5))
|
||||
conn.execute("INSERT INTO balance_history (account_id, balance) VALUES (?, ?)", (aid, 2.5))
|
||||
hist = client.get(f"/api/accounts/{aid}/history", headers=h).json()
|
||||
assert [x["balance"] for x in hist] == [1.5, 2.5]
|
||||
|
||||
|
||||
class TestSettings:
|
||||
def test_update_settings(self, client, tmp_path):
|
||||
h = _auth(client)
|
||||
resp = client.put("/api/settings", json={"global_interval_seconds": 120, "telegram_bot_token": "t:1"}, headers=h)
|
||||
assert resp.status_code == 200
|
||||
cfg = client.get("/api/settings", headers=h).json()
|
||||
assert cfg["global_interval_seconds"] == 120 and cfg["telegram_bot_token"] == "t:1"
|
||||
# 写回 config.json 且可重新加载
|
||||
from app.config import load_config
|
||||
assert load_config().global_interval_seconds == 120
|
||||
|
||||
def test_change_password(self, client):
|
||||
h = _auth(client)
|
||||
assert client.post("/api/settings/password", json={"old_password": "wrong", "new_password": "new123"}, headers=h).status_code == 401
|
||||
assert client.post("/api/settings/password", json={"old_password": "admin123", "new_password": "new123"}, headers=h).status_code == 200
|
||||
assert client.post("/api/login", json={"password": "admin123"}).status_code == 401
|
||||
assert client.post("/api/login", json={"password": "new123"}).status_code == 200
|
||||
Reference in New Issue
Block a user