-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_bot.py
More file actions
214 lines (164 loc) · 8.05 KB
/
Copy pathtest_bot.py
File metadata and controls
214 lines (164 loc) · 8.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""
test_bot.py
-----------
核心邏輯單元測試(全部 mock 掉網路,不會真的打 Yahoo / Gemini)。
執行方式(擇一):
python test_bot.py # 零依賴,直接跑
pytest test_bot.py -v # 需先 pip install pytest
"""
import os
import tempfile
# 測試一律用本機 JSON 後端 + 暫存資料夾,避免動到正式資料
os.environ["DATA_DIR"] = tempfile.mkdtemp(prefix="bottest_")
os.environ.pop("DATABASE_URL", None)
os.environ.setdefault("LINE_CHANNEL_SECRET", "x")
os.environ.setdefault("LINE_CHANNEL_ACCESS_TOKEN", "y")
import services
import store
from linebot.v3.messaging import FlexContainer
# ----------------------------- 工具 ----------------------------- #
def _fake_chart(price=1085.0, prev=1072.0, closes=None, currency="TWD"):
closes = closes or [1000, 1020, 1050, 1072, 1085]
def _fn(symbol, rng="1mo", interval="1d"):
return {
"meta": {"symbol": symbol, "currency": currency,
"regularMarketPrice": price, "previousClose": prev},
"indicators": {"quote": [{
"open": [c - 5 for c in closes], "high": [c + 8 for c in closes],
"low": [c - 10 for c in closes], "close": closes,
"volume": [100] * len(closes)}]},
}
return _fn
def _reset_store():
for uid in ("U1", "U2", "Uwatch", "Ualert", "Ucmd"):
for a in store.list_alerts(uid):
store.remove_alerts([a["id"]])
for w in store.list_watch(uid):
store.remove_watch(uid, w["symbol"])
# ----------------------------- 代號解析 ----------------------------- #
def test_heuristic_taiwan_digits():
cands, name = services.heuristic_candidates("2330")
assert cands == ["2330.TW", "2330.TWO"] and name is None
def test_heuristic_us_letters():
cands, _ = services.heuristic_candidates("aapl")
assert cands == ["AAPL"]
def test_heuristic_chinese_returns_none():
cands, _ = services.heuristic_candidates("台積電")
assert cands is None
def test_expand_candidates_tw_two():
assert services.expand_candidates("2330.TW") == ["2330.TW", "2330.TWO"]
assert services.expand_candidates("6488.TWO") == ["6488.TWO", "6488.TW"]
assert services.expand_candidates("AAPL") == ["AAPL"]
assert services.expand_candidates(None) == []
# ----------------------------- 報價解析 ----------------------------- #
def test_get_quote_parses():
services._yahoo_chart = _fake_chart(price=1085.0, prev=1072.0)
q = services.get_quote("2330.TW")
assert q and q["price"] == 1085.0 and q["prev"] == 1072.0
assert round(q["pct"], 2) == 1.21
assert q["source"] == "yahoo" and len(q["closes"]) == 5
def test_get_quote_none_when_no_data():
services._yahoo_chart = lambda *a, **k: None
assert services.get_quote("BADSYM") is None
def test_lookup_us_ticker():
services._yahoo_chart = _fake_chart(price=212.5, prev=215.0, currency="USD")
q, name = services.lookup("AAPL")
assert q and q["symbol"] == "AAPL" and q["change"] < 0
# ----------------------------- 格式/顏色 ----------------------------- #
def test_formatting_helpers():
assert services._currency_symbol("TWD", "2330.TW") == "NT$"
assert services._currency_symbol("USD", "AAPL") == "$"
assert services._currency_symbol(None, "6488.TWO") == "NT$"
assert services._fmt(1234.5) == "1,234.50"
assert services._fmt(None) == "—"
assert services._accent(5) == services._UP_COLOR
assert services._accent(-5) == services._DOWN_COLOR
assert services._arrow_sign(3)[1] == "+"
# ----------------------------- 走勢圖 ----------------------------- #
def test_sparkline_url():
url = services._sparkline_url([100, 110, 120], up=True)
assert url and url.startswith("https://quickchart.io/chart") and len(url) <= 1900
assert services._sparkline_url([100], up=True) is None
assert services._sparkline_url(None, up=True) is None
# ----------------------------- Flex 圖卡 ----------------------------- #
def test_bubble_has_hero_and_buttons():
services._yahoo_chart = _fake_chart()
q = services.get_quote("2330.TW")
bubble = services.build_stock_bubble("台積電", q, "📰 最新新聞\n• 測試\n\n🔮 展望\n測試")
FlexContainer.from_dict(bubble) # LINE SDK 驗證欄位
assert "hero" in bubble
assert len(bubble["footer"]["contents"][0]["contents"]) == 2 # 兩顆按鈕
def test_bubble_no_hero_for_gemini_source():
q = services.resolve_price_via_gemini # 只是確認函式存在
quote = {"symbol": "AAPL", "price": 200.0, "prev": 198.0, "change": 2.0, "pct": 1.0,
"open": None, "high": None, "low": None, "volume": None,
"currency": "USD", "closes": None, "source": "gemini"}
bubble = services.build_stock_bubble("Apple", quote, None)
FlexContainer.from_dict(bubble)
assert "hero" not in bubble
def test_summary_text_fallback():
services._yahoo_chart = _fake_chart()
q = services.get_quote("2330.TW")
txt = services.stock_summary_text("台積電", q, None)
assert "台積電" in txt and "非投資建議" in txt and "NT$1,085.00" in txt
# ----------------------------- 按鈕 postback ----------------------------- #
def test_postback_watch_and_alert():
_reset_store()
data = services._postback_data("watch", "2330.TW", "台積電")
msg = services.handle_postback(data, "U1")
assert "加入自選" in msg and len(store.list_watch("U1")) == 1
# 重複加入
assert "已在" in services.handle_postback(data, "U1")
# 到價提醒按鈕 → 給設定提示
ad = services._postback_data("alert", "2330.TW", "台積電", 1085.0)
assert "提醒 2330.TW" in services.handle_postback(ad, "U1")
# ----------------------------- 指令 ----------------------------- #
def test_command_help():
assert services.build_reply("", "Ucmd")["text"].startswith("📈")
assert "股票小幫手" in services.build_reply("說明", "Ucmd")["text"]
def test_command_watchlist_empty():
_reset_store()
assert "空的" in services.build_reply("自選", "Ucmd")["text"]
def test_command_set_and_list_alert():
_reset_store()
services._yahoo_chart = _fake_chart(price=1085.0)
r = services.build_reply("提醒 2330 1100", "Ualert")["text"]
assert "已設定提醒" in r
assert len(store.list_alerts("Ualert")) == 1
assert store.list_alerts("Ualert")[0]["dir"] == "above" # 1100 >= 1085
assert "到價提醒" in services.build_reply("提醒清單", "Ualert")["text"]
def test_command_bad_alert_format():
assert "格式" in services.build_reply("提醒 2330", "Ucmd")["text"]
def test_stock_reply_returns_flex_kind():
services._yahoo_chart = _fake_chart()
r = services.build_reply("2330", "Ucmd")
assert r["kind"] == "stock" and r["quote"]["price"] == 1085.0
# ----------------------------- 到價觸發 ----------------------------- #
def test_check_alerts_triggers_and_removes():
_reset_store()
# 目前價 1085,設 above 1100 → 不觸發
services._yahoo_chart = _fake_chart(price=1085.0)
services.build_reply("提醒 2330 1100", "U2")
pushed = []
assert services.check_alerts(lambda uid, t: pushed.append(uid)) == 0
# 價格跳到 1150 → 觸發、推播、刪除
services._yahoo_chart = _fake_chart(price=1150.0, prev=1085.0)
n = services.check_alerts(lambda uid, t: pushed.append(uid))
assert n == 1 and pushed == ["U2"]
assert len(store.list_alerts("U2")) == 0 # 觸發後自動刪除
# ----------------------------- 手動執行器 ----------------------------- #
if __name__ == "__main__":
import sys
tests = [(k, v) for k, v in sorted(globals().items())
if k.startswith("test_") and callable(v)]
failed = 0
print(f"執行 {len(tests)} 個測試…\n")
for name, fn in tests:
try:
fn()
print(f" ✓ {name}")
except Exception as e: # noqa: BLE001
failed += 1
print(f" ✗ {name} → {type(e).__name__}: {e}")
print(f"\n{'✅' if not failed else '❌'} {len(tests) - failed}/{len(tests)} 通過")
sys.exit(1 if failed else 0)