Files
ai-balance-monitor/tests/test_fetcher.py
T

175 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_balancemock 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