05/23 Phase 1

This commit is contained in:
2026-05-23 10:46:51 -04:00
parent a308cfda2d
commit d782ae24b5
9 changed files with 1543 additions and 0 deletions
View File
+38
View File
@@ -0,0 +1,38 @@
"""
tests/conftest.py
-----------------
Shared pytest fixtures for LottoSight test suite.
Uses a temporary in-memory / temp-file SQLite DB so tests
never touch the real lottosight.db.
"""
import os
import sys
import tempfile
import pytest
# Ensure project root is on the path so db/core imports resolve
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@pytest.fixture(scope="function")
def tmp_db(monkeypatch, tmp_path):
"""
Redirect DB_PATH to a fresh temp file for each test function.
Initializes the schema and seeds default games.
Cleaned up automatically by pytest after each test.
"""
db_file = tmp_path / "test_lottosight.db"
# Patch the DB_PATH in database module before init
import db.database as database_module
monkeypatch.setattr(database_module, "DB_PATH", str(db_file))
# Also patch it in models (it imports get_connection which reads DB_PATH)
# get_connection() reads DB_PATH at call time, so patching database_module is enough
from db.database import init_db
init_db()
yield str(db_file)
# tmp_path is auto-cleaned by pytest
+144
View File
@@ -0,0 +1,144 @@
"""
tests/test_database.py
----------------------
Tests for db/database.py:
- init_db() creates all required tables
- Default games are seeded (Powerball, Mega Millions)
- init_db() is idempotent (safe to call multiple times)
- get_db_stats() returns correct draw counts
"""
import pytest
from db.database import init_db, get_db_stats, get_connection
def test_tables_created(tmp_db):
"""All 4 tables must exist after init_db()."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = {row["name"] for row in cursor.fetchall()}
conn.close()
assert "games" in tables, "Missing table: games"
assert "draws" in tables, "Missing table: draws"
assert "predictions" in tables, "Missing table: predictions"
assert "fetch_log" in tables, "Missing table: fetch_log"
def test_default_games_seeded(tmp_db):
"""Powerball and Mega Millions must be seeded after init_db()."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT name FROM games ORDER BY name")
names = [row["name"] for row in cursor.fetchall()]
conn.close()
assert "Powerball" in names, "Powerball not seeded"
assert "Mega Millions" in names, "Mega Millions not seeded"
def test_powerball_config(tmp_db):
"""Powerball config must match spec: 5 balls 1-69, bonus 1-26."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM games WHERE name = 'Powerball'")
row = cursor.fetchone()
conn.close()
assert row is not None
assert row["main_count"] == 5
assert row["main_max"] == 69
assert row["bonus_count"] == 1
assert row["bonus_max"] == 26
assert row["active"] == 1
def test_megamillions_config(tmp_db):
"""Mega Millions config must match spec: 5 balls 1-70, bonus 1-25."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT * FROM games WHERE name = 'Mega Millions'")
row = cursor.fetchone()
conn.close()
assert row is not None
assert row["main_count"] == 5
assert row["main_max"] == 70
assert row["bonus_count"] == 1
assert row["bonus_max"] == 25
assert row["active"] == 1
def test_init_db_idempotent(tmp_db):
"""Calling init_db() multiple times must not raise or duplicate games."""
init_db()
init_db()
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) as cnt FROM games")
count = cursor.fetchone()["cnt"]
conn.close()
assert count == 2, f"Expected 2 games, got {count} — possible duplicate seed"
def test_unique_index_on_draws(tmp_db):
"""Unique index on (game_id, draw_date) must prevent duplicate inserts."""
conn = get_connection()
cursor = conn.cursor()
# Get Powerball id
cursor.execute("SELECT id FROM games WHERE name = 'Powerball'")
pb_id = cursor.fetchone()["id"]
# First insert — should succeed
cursor.execute("""
INSERT INTO draws (game_id, draw_date, numbers, bonus, source)
VALUES (?, ?, ?, ?, ?)
""", (pb_id, "2024-03-01", "5,12,33,47,65", "8", "test"))
conn.commit()
# Duplicate insert — must raise IntegrityError
import sqlite3
with pytest.raises(sqlite3.IntegrityError):
cursor.execute("""
INSERT INTO draws (game_id, draw_date, numbers, bonus, source)
VALUES (?, ?, ?, ?, ?)
""", (pb_id, "2024-03-01", "1,2,3,4,5", "9", "test"))
conn.commit()
conn.close()
def test_get_db_stats_empty(tmp_db):
"""get_db_stats() returns 0 draw count for all games when DB is empty."""
stats = get_db_stats()
assert "Powerball" in stats
assert "Mega Millions" in stats
assert stats["Powerball"] == 0
assert stats["Mega Millions"] == 0
def test_get_db_stats_with_draws(tmp_db):
"""get_db_stats() returns correct count after inserting draws."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("SELECT id FROM games WHERE name = 'Powerball'")
pb_id = cursor.fetchone()["id"]
cursor.execute("""
INSERT INTO draws (game_id, draw_date, numbers, bonus)
VALUES (?, ?, ?, ?)
""", (pb_id, "2024-01-06", "5,12,33,47,65", "8"))
cursor.execute("""
INSERT INTO draws (game_id, draw_date, numbers, bonus)
VALUES (?, ?, ?, ?)
""", (pb_id, "2024-01-10", "2,19,30,44,58", "14"))
conn.commit()
conn.close()
stats = get_db_stats()
assert stats["Powerball"] == 2
assert stats["Mega Millions"] == 0
+385
View File
@@ -0,0 +1,385 @@
"""
tests/test_models.py
--------------------
Tests for db/models.py covering all CRUD functions:
Games — get_all_games, get_game_by_name, get_game_by_id,
set_game_active, add_custom_game
Draws — insert_draw, draw_exists, get_draws, get_last_draw,
get_draw_count, get_all_draws_numbers
Predict — insert_prediction, get_predictions
FetchLog — insert_fetch_log, get_last_fetch_log,
get_fetch_logs, get_last_fetch_per_source
"""
import pytest
from db.models import (
# Games
get_all_games, get_game_by_name, get_game_by_id,
set_game_active, add_custom_game,
# Draws
insert_draw, draw_exists, get_draws, get_last_draw,
get_draw_count, get_all_draws_numbers,
# Predictions
insert_prediction, get_predictions,
# Fetch log
insert_fetch_log, get_last_fetch_log,
get_fetch_logs, get_last_fetch_per_source,
)
# ── Helpers ────────────────────────────────────────────────────────────────
def _pb_id(tmp_db):
"""Return Powerball game id."""
return get_game_by_name("Powerball")["id"]
def _mm_id(tmp_db):
"""Return Mega Millions game id."""
return get_game_by_name("Mega Millions")["id"]
# ══════════════════════════════════════════════════════════════════════════════
# GAMES
# ══════════════════════════════════════════════════════════════════════════════
def test_get_all_games_returns_two(tmp_db):
games = get_all_games()
assert len(games) == 2
def test_get_all_games_active_only(tmp_db):
"""active_only=True should return 2 by default (both active)."""
games = get_all_games(active_only=True)
assert len(games) == 2
def test_get_game_by_name_powerball(tmp_db):
row = get_game_by_name("Powerball")
assert row is not None
assert row["name"] == "Powerball"
def test_get_game_by_name_not_found(tmp_db):
row = get_game_by_name("NonExistentGame")
assert row is None
def test_get_game_by_id(tmp_db):
pb = get_game_by_name("Powerball")
row = get_game_by_id(pb["id"])
assert row is not None
assert row["name"] == "Powerball"
def test_get_game_by_id_not_found(tmp_db):
row = get_game_by_id(9999)
assert row is None
def test_set_game_active_disable(tmp_db):
pb_id = _pb_id(tmp_db)
set_game_active(pb_id, False)
row = get_game_by_id(pb_id)
assert row["active"] == 0
def test_set_game_active_enable(tmp_db):
pb_id = _pb_id(tmp_db)
set_game_active(pb_id, False)
set_game_active(pb_id, True)
row = get_game_by_id(pb_id)
assert row["active"] == 1
def test_active_only_filters_disabled(tmp_db):
pb_id = _pb_id(tmp_db)
set_game_active(pb_id, False)
active_games = get_all_games(active_only=True)
names = [g["name"] for g in active_games]
assert "Powerball" not in names
assert "Mega Millions" in names
def test_add_custom_game(tmp_db):
new_id = add_custom_game("Pick 3", 3, 9, bonus_count=0, bonus_max=0)
assert new_id is not None
row = get_game_by_id(new_id)
assert row["name"] == "Pick 3"
assert row["main_count"] == 3
assert row["main_max"] == 9
assert row["active"] == 1
# ══════════════════════════════════════════════════════════════════════════════
# DRAWS
# ══════════════════════════════════════════════════════════════════════════════
def test_insert_draw_returns_inserted(tmp_db):
pb_id = _pb_id(tmp_db)
result = insert_draw(pb_id, "2024-01-06", [5, 12, 33, 47, 65],
bonus=8, source="test")
assert result == "inserted"
def test_insert_draw_duplicate_returns_skipped(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-06", [5, 12, 33, 47, 65], bonus=8)
result = insert_draw(pb_id, "2024-01-06", [1, 2, 3, 4, 5], bonus=9)
assert result == "skipped"
def test_insert_draw_accepts_string_numbers(tmp_db):
pb_id = _pb_id(tmp_db)
result = insert_draw(pb_id, "2024-02-01", "10,20,30,40,50", bonus="5")
assert result == "inserted"
def test_draw_exists_true(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-06", [5, 12, 33, 47, 65], bonus=8)
assert draw_exists(pb_id, "2024-01-06") is True
def test_draw_exists_false(tmp_db):
pb_id = _pb_id(tmp_db)
assert draw_exists(pb_id, "2099-12-31") is False
def test_get_draws_returns_correct_game(tmp_db):
pb_id = _pb_id(tmp_db)
mm_id = _mm_id(tmp_db)
insert_draw(pb_id, "2024-01-06", [5, 12, 33, 47, 65], bonus=8)
insert_draw(mm_id, "2024-01-05", [3, 17, 28, 41, 60], bonus=12)
pb_draws = get_draws(pb_id)
mm_draws = get_draws(mm_id)
assert len(pb_draws) == 1
assert len(mm_draws) == 1
def test_get_draws_order_desc(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-01", [1, 2, 3, 4, 5], bonus=1)
insert_draw(pb_id, "2024-01-10", [6, 7, 8, 9, 10], bonus=2)
insert_draw(pb_id, "2024-01-20", [11, 12, 13, 14, 15], bonus=3)
draws = get_draws(pb_id, order="DESC")
dates = [d["draw_date"] for d in draws]
assert dates == sorted(dates, reverse=True)
def test_get_draws_order_asc(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-01", [1, 2, 3, 4, 5], bonus=1)
insert_draw(pb_id, "2024-01-10", [6, 7, 8, 9, 10], bonus=2)
draws = get_draws(pb_id, order="ASC")
dates = [d["draw_date"] for d in draws]
assert dates == sorted(dates)
def test_get_draws_with_limit(tmp_db):
pb_id = _pb_id(tmp_db)
for i in range(1, 6):
insert_draw(pb_id, f"2024-01-{i:02d}", [i, i+1, i+2, i+3, i+4], bonus=i)
draws = get_draws(pb_id, limit=3)
assert len(draws) == 3
def test_get_draws_date_filter(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-01", [1, 2, 3, 4, 5], bonus=1)
insert_draw(pb_id, "2024-06-15", [6, 7, 8, 9, 10], bonus=2)
insert_draw(pb_id, "2024-12-31", [11, 12, 13, 14, 15], bonus=3)
draws = get_draws(pb_id, date_from="2024-06-01", date_to="2024-12-01")
assert len(draws) == 1
assert draws[0]["draw_date"] == "2024-06-15"
def test_get_last_draw(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-01", [1, 2, 3, 4, 5], bonus=1)
insert_draw(pb_id, "2024-01-20", [6, 7, 8, 9, 10], bonus=2)
last = get_last_draw(pb_id)
assert last["draw_date"] == "2024-01-20"
def test_get_last_draw_empty(tmp_db):
pb_id = _pb_id(tmp_db)
assert get_last_draw(pb_id) is None
def test_get_draw_count(tmp_db):
pb_id = _pb_id(tmp_db)
assert get_draw_count(pb_id) == 0
insert_draw(pb_id, "2024-01-01", [1, 2, 3, 4, 5], bonus=1)
insert_draw(pb_id, "2024-01-10", [6, 7, 8, 9, 10], bonus=2)
assert get_draw_count(pb_id) == 2
def test_get_all_draws_numbers_parses_correctly(tmp_db):
pb_id = _pb_id(tmp_db)
insert_draw(pb_id, "2024-01-06", [5, 12, 33, 47, 65], bonus=8)
insert_draw(pb_id, "2024-01-10", [2, 19, 30, 44, 58], bonus=14)
draws = get_all_draws_numbers(pb_id)
assert len(draws) == 2
# Oldest first (ASC)
assert draws[0]["draw_date"] == "2024-01-06"
assert draws[0]["numbers"] == [5, 12, 33, 47, 65]
assert draws[0]["bonus"] == 8
assert draws[1]["draw_date"] == "2024-01-10"
assert draws[1]["numbers"] == [2, 19, 30, 44, 58]
assert draws[1]["bonus"] == 14
def test_get_all_draws_numbers_empty(tmp_db):
pb_id = _pb_id(tmp_db)
draws = get_all_draws_numbers(pb_id)
assert draws == []
# ══════════════════════════════════════════════════════════════════════════════
# PREDICTIONS
# ══════════════════════════════════════════════════════════════════════════════
def test_insert_prediction_returns_id(tmp_db):
pb_id = _pb_id(tmp_db)
new_id = insert_prediction(pb_id, "Hot Numbers", [7, 14, 22, 36, 55], bonus=18)
assert isinstance(new_id, int)
assert new_id > 0
def test_insert_prediction_list_and_string(tmp_db):
pb_id = _pb_id(tmp_db)
id1 = insert_prediction(pb_id, "Hot Numbers", [1, 2, 3, 4, 5], bonus=6)
id2 = insert_prediction(pb_id, "Due Numbers", "10,20,30,40,50", bonus=None)
assert id1 != id2
def test_get_predictions_by_game(tmp_db):
pb_id = _pb_id(tmp_db)
mm_id = _mm_id(tmp_db)
insert_prediction(pb_id, "Hot Numbers", [1, 2, 3, 4, 5], bonus=6)
insert_prediction(mm_id, "Monte Carlo", [10, 20, 30, 40, 50], bonus=7)
pb_preds = get_predictions(game_id=pb_id)
mm_preds = get_predictions(game_id=mm_id)
assert len(pb_preds) == 1
assert len(mm_preds) == 1
assert pb_preds[0]["strategy"] == "Hot Numbers"
assert mm_preds[0]["strategy"] == "Monte Carlo"
def test_get_predictions_all_games(tmp_db):
pb_id = _pb_id(tmp_db)
mm_id = _mm_id(tmp_db)
insert_prediction(pb_id, "Hot Numbers", [1, 2, 3, 4, 5])
insert_prediction(mm_id, "Due Numbers", [6, 7, 8, 9, 10])
all_preds = get_predictions()
assert len(all_preds) == 2
def test_get_predictions_respects_limit(tmp_db):
pb_id = _pb_id(tmp_db)
for i in range(10):
insert_prediction(pb_id, "Weighted Random", [i+1, i+2, i+3, i+4, i+5])
preds = get_predictions(game_id=pb_id, limit=5)
assert len(preds) == 5
def test_get_predictions_newest_first(tmp_db):
"""
Verify get_predictions returns DESC order by id (newest first).
We can't rely on created_at within the same second in SQLite,
so compare by id: higher id = inserted later = should appear first.
"""
pb_id = _pb_id(tmp_db)
id1 = insert_prediction(pb_id, "Hot Numbers", [1, 2, 3, 4, 5])
id2 = insert_prediction(pb_id, "Due Numbers", [6, 7, 8, 9, 10])
preds = get_predictions(game_id=pb_id)
ids = [p["id"] for p in preds]
# Should be descending: id2 before id1
assert ids.index(id2) < ids.index(id1), (
"Predictions not returned newest-first by id"
)
# ══════════════════════════════════════════════════════════════════════════════
# FETCH LOG
# ══════════════════════════════════════════════════════════════════════════════
def test_insert_fetch_log_returns_id(tmp_db):
new_id = insert_fetch_log("NY Powerball", added=10, skipped=2)
assert isinstance(new_id, int)
assert new_id > 0
def test_insert_fetch_log_error_status(tmp_db):
new_id = insert_fetch_log("TX Mega Millions", added=0, skipped=0,
status="error", message="Connection timeout")
assert new_id > 0
def test_get_last_fetch_log_any(tmp_db):
"""
get_last_fetch_log() with no source filter returns the most recent entry.
Within the same second, SQLite order is undefined — use id to verify.
"""
id1 = insert_fetch_log("NY Powerball", added=5, skipped=1)
id2 = insert_fetch_log("NY Mega Millions", added=3, skipped=0)
row = get_last_fetch_log()
assert row is not None
# Most recent = highest id
assert row["id"] == max(id1, id2)
def test_get_last_fetch_log_by_source(tmp_db):
insert_fetch_log("NY Powerball", added=5, skipped=1)
insert_fetch_log("NY Mega Millions", added=3, skipped=0)
row = get_last_fetch_log(source="NY Powerball")
assert row is not None
assert row["source"] == "NY Powerball"
assert row["added"] == 5
assert row["skipped"] == 1
def test_get_last_fetch_log_none_when_empty(tmp_db):
row = get_last_fetch_log()
assert row is None
def test_get_fetch_logs_limit(tmp_db):
for i in range(10):
insert_fetch_log(f"source_{i}", added=i, skipped=0)
logs = get_fetch_logs(limit=5)
assert len(logs) == 5
def test_get_last_fetch_per_source(tmp_db):
insert_fetch_log("NY Powerball", added=5, skipped=1)
insert_fetch_log("NY Mega Millions", added=3, skipped=0)
insert_fetch_log("TX Mega Millions", added=2, skipped=1)
# Second Powerball fetch — should be the "latest" for that source
insert_fetch_log("NY Powerball", added=1, skipped=4)
per_source = get_last_fetch_per_source()
assert "NY Powerball" in per_source
assert "NY Mega Millions" in per_source
assert "TX Mega Millions" in per_source
# Latest Powerball fetch had added=1
assert per_source["NY Powerball"]["added"] == 1
def test_get_last_fetch_per_source_empty(tmp_db):
per_source = get_last_fetch_per_source()
assert per_source == {}