05/23 Phase 22
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
tests/test_phase22.py
|
||||
----------------------
|
||||
Tests for Phase 22 features:
|
||||
- Incremental VA fetch (break on already-stored dates)
|
||||
- top_prize column in games table
|
||||
- Dashboard game filter
|
||||
- Auto-check predictions after fetch (match detection logic)
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from db.database import get_db_stats, init_db
|
||||
from db.models import (
|
||||
get_all_games,
|
||||
get_game_by_name,
|
||||
get_last_draw,
|
||||
insert_draw,
|
||||
insert_prediction,
|
||||
get_predictions,
|
||||
)
|
||||
from core.fetcher import fetch_cash5_va, fetch_bankamillion_va
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _text_resp(text, status=200):
|
||||
m = MagicMock()
|
||||
m.status_code = status
|
||||
m.raise_for_status = MagicMock()
|
||||
m.text = text
|
||||
return m
|
||||
|
||||
|
||||
CASH5_VA_TEXT_OLD = (
|
||||
"5/22/2026; 15,29,30,34,36\n"
|
||||
"5/21/2026; 1,2,5,38,44\n"
|
||||
)
|
||||
|
||||
CASH5_VA_TEXT_NEWER = (
|
||||
"5/23/2026; 10,11,12,13,14\n" # new record
|
||||
"5/22/2026; 15,29,30,34,36\n" # already in DB
|
||||
"5/21/2026; 1,2,5,38,44\n" # already in DB
|
||||
)
|
||||
|
||||
|
||||
# ── top_prize column ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_top_prize_seeded_powerball(tmp_db):
|
||||
row = get_game_by_name("Powerball")
|
||||
assert row["top_prize"] == "Jackpot (variable)"
|
||||
|
||||
|
||||
def test_top_prize_seeded_mega_millions(tmp_db):
|
||||
row = get_game_by_name("Mega Millions")
|
||||
assert row["top_prize"] == "Jackpot (variable)"
|
||||
|
||||
|
||||
def test_top_prize_seeded_cash5(tmp_db):
|
||||
row = get_game_by_name("Cash 5")
|
||||
assert row["top_prize"] == "Jackpot from $200K"
|
||||
|
||||
|
||||
def test_top_prize_seeded_millionaire_for_life(tmp_db):
|
||||
row = get_game_by_name("Millionaire for Life")
|
||||
assert row["top_prize"] == "$1M/yr for life"
|
||||
|
||||
|
||||
def test_top_prize_seeded_bank_a_million(tmp_db):
|
||||
row = get_game_by_name("Bank a Million")
|
||||
assert row["top_prize"] == "$1M after taxes"
|
||||
|
||||
|
||||
def test_top_prize_all_games_non_empty(tmp_db):
|
||||
games = get_all_games()
|
||||
for g in games:
|
||||
# Builtin games must have a top_prize; custom games may be empty
|
||||
assert "top_prize" in g.keys()
|
||||
|
||||
|
||||
# ── Incremental VA fetch ───────────────────────────────────────────────────────
|
||||
|
||||
def test_incremental_fetch_only_new_records_added(tmp_db):
|
||||
"""Second fetch with a new leading record only inserts that one record."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
r1 = fetch_cash5_va()
|
||||
assert r1["added"] == 2
|
||||
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_NEWER)):
|
||||
r2 = fetch_cash5_va()
|
||||
assert r2["added"] == 1
|
||||
assert r2["skipped"] == 0 # stopped before re-attempting stored dates
|
||||
|
||||
|
||||
def test_incremental_fetch_no_new_data(tmp_db):
|
||||
"""Second fetch of identical data adds nothing and doesn't error."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
fetch_cash5_va()
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
r = fetch_cash5_va()
|
||||
assert r["status"] == "success"
|
||||
assert r["added"] == 0
|
||||
|
||||
|
||||
def test_incremental_fetch_db_count_correct(tmp_db):
|
||||
"""Total draws in DB after incremental fetch equals unique dates only."""
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_OLD)):
|
||||
fetch_cash5_va()
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(CASH5_VA_TEXT_NEWER)):
|
||||
fetch_cash5_va()
|
||||
|
||||
game = get_game_by_name("Cash 5")
|
||||
stats = get_db_stats()
|
||||
assert stats["Cash 5"] == 3 # 2026-05-21, 05-22, 05-23
|
||||
|
||||
|
||||
def test_incremental_fetch_bank_a_million(tmp_db):
|
||||
"""Bank a Million incremental fetch behaves the same as Cash 5."""
|
||||
bam_old = (
|
||||
"Results for Bank a Million\n"
|
||||
"5/20/2026; 14,20,21,24,33,35; Bonus Ball: 18\n"
|
||||
"5/16/2026; 6,8,17,20,29,39; Bonus Ball: 38\n"
|
||||
)
|
||||
bam_newer = (
|
||||
"Results for Bank a Million\n"
|
||||
"5/23/2026; 1,5,10,20,35,38; Bonus Ball: 7\n"
|
||||
"5/20/2026; 14,20,21,24,33,35; Bonus Ball: 18\n"
|
||||
"5/16/2026; 6,8,17,20,29,39; Bonus Ball: 38\n"
|
||||
)
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(bam_old)):
|
||||
r1 = fetch_bankamillion_va()
|
||||
assert r1["added"] == 2
|
||||
|
||||
with patch("core.fetcher.requests.get", return_value=_text_resp(bam_newer)):
|
||||
r2 = fetch_bankamillion_va()
|
||||
assert r2["added"] == 1
|
||||
assert r2["skipped"] == 0
|
||||
|
||||
|
||||
# ── Match detection logic (unit-level) ────────────────────────────────────────
|
||||
|
||||
def _count_matches(pred_numbers: str, draw_numbers: str) -> int:
|
||||
pred = {int(n) for n in pred_numbers.split(",") if n.strip().isdigit()}
|
||||
draw = {int(n) for n in draw_numbers.split(",") if n.strip().isdigit()}
|
||||
return len(pred & draw)
|
||||
|
||||
|
||||
def test_match_detection_exact(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
pred_id = insert_prediction(pb["id"], "Hot Numbers", [1, 13, 36, 61, 69], bonus=7)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
pred = preds[0]
|
||||
|
||||
matches = _count_matches(pred["numbers"], last["numbers"])
|
||||
assert matches == 5
|
||||
|
||||
|
||||
def test_match_detection_partial(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
insert_prediction(pb["id"], "Hot Numbers", [1, 13, 5, 6, 7], bonus=99)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
|
||||
matches = _count_matches(preds[0]["numbers"], last["numbers"])
|
||||
assert matches == 2
|
||||
|
||||
|
||||
def test_match_detection_no_match(tmp_db):
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
insert_prediction(pb["id"], "Due Numbers", [2, 4, 6, 8, 10], bonus=99)
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
|
||||
matches = _count_matches(preds[0]["numbers"], last["numbers"])
|
||||
assert matches == 0
|
||||
|
||||
|
||||
def test_match_alert_threshold_is_two(tmp_db):
|
||||
"""Only predictions with ≥2 main matches (or bonus hit) count toward alert."""
|
||||
pb = get_game_by_name("Powerball")
|
||||
insert_draw(pb["id"], "2026-05-22", [1, 13, 36, 61, 69], bonus=7)
|
||||
last = get_last_draw(pb["id"])
|
||||
|
||||
preds_data = [
|
||||
([1, 2, 3, 4, 5], 99), # 1 match — below threshold
|
||||
([1, 13, 2, 3, 4], 99), # 2 matches — at threshold
|
||||
([1, 13, 36, 2, 3], 99), # 3 matches — above threshold
|
||||
]
|
||||
for nums, bonus in preds_data:
|
||||
insert_prediction(pb["id"], "Test", nums, bonus=bonus)
|
||||
|
||||
preds = get_predictions(game_id=pb["id"])
|
||||
draw_nums = {int(n) for n in last["numbers"].split(",") if n.strip().isdigit()}
|
||||
|
||||
qualifying = [
|
||||
p for p in preds
|
||||
if len({int(n) for n in p["numbers"].split(",") if n.strip().isdigit()} & draw_nums) >= 2
|
||||
]
|
||||
assert len(qualifying) == 2
|
||||
Reference in New Issue
Block a user