feat: AI 余额监控服务(平台/账号 CRUD、并发轮询、Telegram 阈值提醒)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_db(tmp_path, monkeypatch):
|
||||
"""把数据库指向临时目录,避免污染真实 data/monitor.db。"""
|
||||
from app import db
|
||||
|
||||
monkeypatch.setattr(db, "DB_PATH", tmp_path / "test.db")
|
||||
monkeypatch.setattr(db, "DATA_DIR", tmp_path)
|
||||
db.init_db()
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_platform(test_db):
|
||||
"""插入一个示例平台,返回 dict。"""
|
||||
from app import db
|
||||
|
||||
with db.get_conn() as conn:
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO platforms (name, currency, icon, method, url, headers, body,
|
||||
balance_path, interval_seconds, retry_count, timeout_seconds, enabled, note)
|
||||
VALUES ('OpenAI', 'USD', 'OpenAI', 'GET', 'https://x.test/api?key={{apiKey}}',
|
||||
'{"Authorization": "Bearer {{apiKey}}"}', '', 'data.balance', 60, 2, 10, 1, '')"""
|
||||
)
|
||||
pid = cur.lastrowid
|
||||
return dict((test_db.get_conn().execute("SELECT * FROM platforms WHERE id=?", (pid,)).fetchone()))
|
||||
@@ -0,0 +1,114 @@
|
||||
"""提醒状态机测试:低于阈值提醒一次、恢复后重新武装、发送失败不切换状态。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app import db
|
||||
from app.alert import build_alert_message, evaluate, notify_disabled
|
||||
|
||||
ACCOUNT = {
|
||||
"id": 1, "name": "主账号", "threshold": 20.0, "alert_armed": 1,
|
||||
"last_check_at": "2026-08-04 10:00:00",
|
||||
}
|
||||
PLATFORM = {"name": "OpenAI", "currency": "USD"}
|
||||
|
||||
|
||||
def _insert_account(test_db, armed=1, enabled=1):
|
||||
with db.get_conn() as conn:
|
||||
conn.execute("INSERT INTO platforms (id, name, url, balance_path) VALUES (1, 'OpenAI', 'http://x', 'b')")
|
||||
conn.execute(
|
||||
"""INSERT INTO accounts (id, platform_id, name, api_key, threshold, enabled, alert_armed)
|
||||
VALUES (1, 1, '主账号', 'a2tva2V5', 20.0, ?, ?)""",
|
||||
(enabled, armed),
|
||||
)
|
||||
|
||||
|
||||
def _armed(test_db):
|
||||
with db.get_conn() as conn:
|
||||
return conn.execute("SELECT alert_armed FROM accounts WHERE id=1").fetchone()[0]
|
||||
|
||||
|
||||
def _log(test_db):
|
||||
with db.get_conn() as conn:
|
||||
return conn.execute("SELECT type FROM alert_log WHERE account_id=1 ORDER BY id").fetchall()
|
||||
|
||||
|
||||
class TestEvaluate:
|
||||
def test_below_threshold_armed_sends_and_disarms(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=1)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(a) or True)
|
||||
evaluate(ACCOUNT, PLATFORM, 12.5, type("Cfg", (), {"telegram_bot_token": "t", "telegram_chat_id": "c"})())
|
||||
assert len(sent) == 1
|
||||
assert "12.5" in sent[0][2] and "OpenAI" in sent[0][2]
|
||||
assert _armed(test_db) == 0
|
||||
assert [r[0] for r in _log(test_db)] == ["below"]
|
||||
|
||||
def test_still_below_no_second_alert(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=0)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(1) or True)
|
||||
evaluate(dict(ACCOUNT, alert_armed=0), PLATFORM, 5.0, None)
|
||||
assert sent == []
|
||||
|
||||
def test_recover_after_disarmed(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=0)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(a[2]) or True)
|
||||
evaluate(dict(ACCOUNT, alert_armed=0), PLATFORM, 30.0, type("Cfg", (), {"telegram_bot_token": "t", "telegram_chat_id": "c"})())
|
||||
assert len(sent) == 1 and "恢复" in sent[0]
|
||||
assert _armed(test_db) == 1
|
||||
|
||||
def test_above_threshold_armed_no_alert(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=1)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(1) or True)
|
||||
evaluate(ACCOUNT, PLATFORM, 99.0, None)
|
||||
assert sent == []
|
||||
assert _armed(test_db) == 1
|
||||
|
||||
def test_exactly_threshold_is_not_below(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=1)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(1) or True)
|
||||
evaluate(ACCOUNT, PLATFORM, 20.0, None)
|
||||
assert sent == []
|
||||
|
||||
def test_send_failure_keeps_armed(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=1)
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: False)
|
||||
evaluate(ACCOUNT, PLATFORM, 5.0, type("Cfg", (), {"telegram_bot_token": "t", "telegram_chat_id": "c"})())
|
||||
assert _armed(test_db) == 1 # 发送失败 → 保持武装,下次再试
|
||||
assert _log(test_db) == []
|
||||
|
||||
def test_full_cycle(self, test_db, monkeypatch):
|
||||
_insert_account(test_db, armed=1)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(a[2]) or True)
|
||||
cfg = type("Cfg", (), {"telegram_bot_token": "t", "telegram_chat_id": "c"})()
|
||||
evaluate(ACCOUNT, PLATFORM, 8.0, cfg)
|
||||
evaluate(dict(ACCOUNT, alert_armed=0), PLATFORM, 8.0, cfg) # 仍低于 → 不再发
|
||||
evaluate(dict(ACCOUNT, alert_armed=0), PLATFORM, 50.0, cfg) # 恢复 → 发恢复
|
||||
evaluate(dict(ACCOUNT, alert_armed=1), PLATFORM, 60.0, cfg) # 正常 → 不发
|
||||
evaluate(dict(ACCOUNT, alert_armed=1), PLATFORM, 3.0, cfg) # 又低于 → 再提醒
|
||||
assert len(sent) == 3
|
||||
assert [r[0] for r in _log(test_db)] == ["below", "recovered", "below"]
|
||||
|
||||
|
||||
class TestNotifyDisabled:
|
||||
def test_disabled_notification(self, test_db, monkeypatch):
|
||||
_insert_account(test_db)
|
||||
sent = []
|
||||
monkeypatch.setattr("app.alert.send_message", lambda *a: sent.append(a[2]) or True)
|
||||
notify_disabled(dict(ACCOUNT, last_error="HTTP 401(认证失败)"), PLATFORM,
|
||||
type("Cfg", (), {"telegram_bot_token": "t", "telegram_chat_id": "c"})())
|
||||
assert len(sent) == 1 and "禁用" in sent[0] and "401" in sent[0]
|
||||
assert [r[0] for r in _log(test_db)] == ["disabled"]
|
||||
|
||||
|
||||
class TestBuildMessage:
|
||||
def test_below_message_content(self):
|
||||
msg = build_alert_message("below", ACCOUNT, PLATFORM, 12.5)
|
||||
assert "12.5 USD" in msg and "20 USD" in msg and "主账号" in msg
|
||||
|
||||
def test_unknown_kind_empty(self):
|
||||
assert build_alert_message("nope", ACCOUNT, PLATFORM, 1) == ""
|
||||
@@ -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
|
||||
@@ -0,0 +1,174 @@
|
||||
"""fetcher 核心逻辑测试:路径提取、模板渲染、请求/重试策略。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.fetcher import extract_balance, fetch_balance, render_template
|
||||
|
||||
|
||||
# ---------- extract_balance ----------
|
||||
|
||||
class TestExtractBalance:
|
||||
def test_dot_path(self):
|
||||
assert extract_balance({"data": {"balance": 12.5}}, "data.balance") == 12.5
|
||||
|
||||
def test_array_index(self):
|
||||
assert extract_balance({"data": [{"balance": 3.0}]}, "data[0].balance") == 3.0
|
||||
|
||||
def test_dollar_prefix(self):
|
||||
assert extract_balance({"a": {"b": 7}}, "$.a.b") == 7
|
||||
|
||||
def test_root_key(self):
|
||||
assert extract_balance({"balance": 1}, "balance") == 1
|
||||
|
||||
def test_nested_array(self):
|
||||
data = {"list": [[1, 2], [3, 4]]}
|
||||
assert extract_balance(data, "list[1][0]") == 3
|
||||
|
||||
def test_string_number(self):
|
||||
assert extract_balance({"data": {"balance": "9.99"}}, "data.balance") == 9.99
|
||||
|
||||
def test_missing_key_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
extract_balance({"data": {}}, "data.balance")
|
||||
|
||||
def test_index_out_of_range_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
extract_balance({"data": []}, "data[0].balance")
|
||||
|
||||
def test_non_numeric_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
extract_balance({"data": {"balance": "abc"}}, "data.balance")
|
||||
|
||||
def test_bool_rejected(self):
|
||||
with pytest.raises(ValueError):
|
||||
extract_balance({"data": {"balance": True}}, "data.balance")
|
||||
|
||||
|
||||
# ---------- render_template ----------
|
||||
|
||||
class TestRenderTemplate:
|
||||
def test_replace(self):
|
||||
assert render_template("https://x?key={{apiKey}}&a=1", "sk-123") == "https://x?key=sk-123&a=1"
|
||||
|
||||
def test_multiple(self):
|
||||
assert render_template("{{apiKey}}/{{apiKey}}", "k") == "k/k"
|
||||
|
||||
|
||||
# ---------- fetch_balance(mock requests) ----------
|
||||
|
||||
class FakeResponse:
|
||||
def __init__(self, status_code=200, text="", json_data=None):
|
||||
self.status_code = status_code
|
||||
self.text = text
|
||||
self._json = json_data
|
||||
|
||||
def json(self):
|
||||
if self._json is None:
|
||||
raise ValueError("No JSON object could be decoded")
|
||||
return self._json
|
||||
|
||||
|
||||
PLATFORM_GET = {
|
||||
"method": "GET",
|
||||
"url": "https://x.test/api?key={{apiKey}}",
|
||||
"headers": {"Authorization": "Bearer {{apiKey}}"},
|
||||
"body": "",
|
||||
"balance_path": "data.balance",
|
||||
}
|
||||
|
||||
|
||||
class TestFetchBalance:
|
||||
def test_get_success(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_get(url, headers=None, timeout=10):
|
||||
calls.append((url, headers))
|
||||
assert url == "https://x.test/api?key=sk-1"
|
||||
assert headers["Authorization"] == "Bearer sk-1"
|
||||
return FakeResponse(200, json_data={"data": {"balance": 42.5}})
|
||||
|
||||
monkeypatch.setattr("requests.get", fake_get)
|
||||
result = fetch_balance(PLATFORM_GET, "sk-1", retry_count=2, timeout=10)
|
||||
assert result.ok and result.balance == 42.5
|
||||
assert len(calls) == 1
|
||||
|
||||
def test_post_with_body(self, monkeypatch):
|
||||
platform = dict(PLATFORM_GET, method="POST", body='{"api_key": "{{apiKey}}"}')
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, headers=None, data=None, timeout=10):
|
||||
captured["data"] = data
|
||||
return FakeResponse(200, json_data={"data": {"balance": 1}})
|
||||
|
||||
monkeypatch.setattr("requests.post", fake_post)
|
||||
result = fetch_balance(platform, "sk-2", retry_count=2, timeout=10)
|
||||
assert result.ok
|
||||
assert captured["data"] == '{"api_key": "sk-2"}'
|
||||
|
||||
def test_401_is_auth_error_no_retry(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_get(url, headers=None, timeout=10):
|
||||
calls.append(1)
|
||||
return FakeResponse(401, text="unauthorized")
|
||||
|
||||
monkeypatch.setattr("requests.get", fake_get)
|
||||
result = fetch_balance(PLATFORM_GET, "bad", retry_count=3, timeout=10)
|
||||
assert not result.ok and result.auth_error
|
||||
assert len(calls) == 1
|
||||
|
||||
def test_5xx_retries_then_success(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_get(url, headers=None, timeout=10):
|
||||
calls.append(1)
|
||||
return FakeResponse(500) if len(calls) < 3 else FakeResponse(200, json_data={"data": {"balance": 5}})
|
||||
|
||||
monkeypatch.setattr("requests.get", fake_get)
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=2, timeout=10)
|
||||
assert result.ok and result.balance == 5
|
||||
assert len(calls) == 3
|
||||
|
||||
def test_5xx_all_fail(self, monkeypatch):
|
||||
monkeypatch.setattr("requests.get", lambda *a, **k: FakeResponse(503))
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=2, timeout=10)
|
||||
assert not result.ok and not result.auth_error
|
||||
|
||||
def test_network_error_retries(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_get(url, headers=None, timeout=10):
|
||||
calls.append(1)
|
||||
import requests
|
||||
if len(calls) < 3:
|
||||
raise requests.ConnectionError("boom")
|
||||
return FakeResponse(200, json_data={"data": {"balance": 8}})
|
||||
|
||||
monkeypatch.setattr("requests.get", fake_get)
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=2, timeout=10)
|
||||
assert result.ok and result.balance == 8
|
||||
|
||||
def test_other_4xx_no_retry(self, monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake_get(url, headers=None, timeout=10):
|
||||
calls.append(1)
|
||||
return FakeResponse(404, text="not found")
|
||||
|
||||
monkeypatch.setattr("requests.get", fake_get)
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=2, timeout=10)
|
||||
assert not result.ok and not result.auth_error
|
||||
assert "404" in result.error
|
||||
assert len(calls) == 1
|
||||
|
||||
def test_bad_json_path(self, monkeypatch):
|
||||
monkeypatch.setattr("requests.get", lambda *a, **k: FakeResponse(200, json_data={"x": 1}))
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=0, timeout=10)
|
||||
assert not result.ok
|
||||
assert "路径不存在" in result.error
|
||||
|
||||
def test_invalid_json_body(self, monkeypatch):
|
||||
monkeypatch.setattr("requests.get", lambda *a, **k: FakeResponse(200, text="<html>"))
|
||||
result = fetch_balance(PLATFORM_GET, "k", retry_count=0, timeout=10)
|
||||
assert not result.ok
|
||||
assert "JSON" in result.error
|
||||
Reference in New Issue
Block a user