""" tests/test_fetcher.py --------------------- Tests for core/fetcher.py — all HTTP calls are mocked. """ import json from unittest.mock import MagicMock, patch import pytest import requests as req_lib from core.fetcher import ( fetch_all, fetch_megamillions_ny, fetch_megamillions_tx, fetch_powerball_ny, ) from db.models import get_draw_count, get_draws, get_game_by_name, get_last_fetch_log # ── Mock data ───────────────────────────────────────────────────────────────── PB_NY_RECORDS = [ {"draw_date": "2024-01-01T00:00:00.000", "winning_numbers": "01 13 36 61 69 07", "multiplier": "2"}, {"draw_date": "2024-01-03T00:00:00.000", "winning_numbers": "05 20 45 60 68 15", "multiplier": "3"}, ] MM_NY_RECORDS = [ {"draw_date": "2024-02-01T00:00:00.000", "winning_numbers": "07 11 22 29 38", "mega_ball": "04", "multiplier": "2"}, {"draw_date": "2024-02-05T00:00:00.000", "winning_numbers": "03 18 33 44 67", "mega_ball": "22", "multiplier": "5"}, ] MM_TX_CSV = ( "Game Name,Month,Day,Year,Num1,Num2,Num3,Num4,Num5,Mega Ball,Megaplier\n" "Mega Millions,03,07,2024,10,20,30,40,50,12,3\n" "Mega Millions,03,12,2024,15,25,35,45,55,07,4\n" ) def _json_resp(data, status=200): """Mock requests.Response returning JSON.""" m = MagicMock() m.status_code = status m.raise_for_status = MagicMock() m.json.return_value = data m.text = json.dumps(data) return m def _text_resp(text, status=200): """Mock requests.Response returning plain text (CSV).""" m = MagicMock() m.status_code = status m.raise_for_status = MagicMock() m.text = text return m # ── Powerball NY ────────────────────────────────────────────────────────────── def test_pb_ny_inserts_records(tmp_db): with patch("core.fetcher.requests.get", side_effect=[_json_resp(PB_NY_RECORDS), _json_resp([])]): result = fetch_powerball_ny() assert result["status"] == "success" assert result["added"] == 2 assert result["skipped"] == 0 game = get_game_by_name("Powerball") assert get_draw_count(game["id"]) == 2 def test_pb_ny_skips_duplicates(tmp_db): for _ in range(2): with patch("core.fetcher.requests.get", side_effect=[_json_resp(PB_NY_RECORDS), _json_resp([])]): result = fetch_powerball_ny() assert result["added"] == 0 assert result["skipped"] == 2 def test_pb_ny_parses_numbers_and_bonus(tmp_db): with patch("core.fetcher.requests.get", side_effect=[_json_resp(PB_NY_RECORDS[:1]), _json_resp([])]): fetch_powerball_ny() game = get_game_by_name("Powerball") draws = get_draws(game["id"]) assert draws[0]["draw_date"] == "2024-01-01" assert draws[0]["numbers"] == "1,13,36,61,69" assert draws[0]["bonus"] == "7" assert draws[0]["multiplier"] == "2" assert draws[0]["source"] == "powerball_ny" def test_pb_ny_network_error(tmp_db): with patch("core.fetcher.requests.get", side_effect=req_lib.RequestException("timeout")): result = fetch_powerball_ny() assert result["status"] == "error" assert result["added"] == 0 assert "timeout" in result["message"] log = get_last_fetch_log("powerball_ny") assert log["status"] == "error" def test_pb_ny_malformed_row_skipped(tmp_db): """Row with too few numbers is skipped without crashing.""" bad_records = [{"draw_date": "2024-01-01T00:00:00.000", "winning_numbers": "01 13 36"}] with patch("core.fetcher.requests.get", side_effect=[_json_resp(bad_records), _json_resp([])]): result = fetch_powerball_ny() assert result["status"] == "success" assert result["added"] == 0 # ── Mega Millions NY ────────────────────────────────────────────────────────── def test_mm_ny_inserts_records(tmp_db): with patch("core.fetcher.requests.get", side_effect=[_json_resp(MM_NY_RECORDS), _json_resp([])]): result = fetch_megamillions_ny() assert result["status"] == "success" assert result["added"] == 2 assert result["skipped"] == 0 def test_mm_ny_skips_duplicates(tmp_db): for _ in range(2): with patch("core.fetcher.requests.get", side_effect=[_json_resp(MM_NY_RECORDS), _json_resp([])]): result = fetch_megamillions_ny() assert result["added"] == 0 assert result["skipped"] == 2 def test_mm_ny_parses_mega_ball(tmp_db): with patch("core.fetcher.requests.get", side_effect=[_json_resp(MM_NY_RECORDS[:1]), _json_resp([])]): fetch_megamillions_ny() game = get_game_by_name("Mega Millions") draws = get_draws(game["id"]) assert draws[0]["bonus"] == "4" assert draws[0]["numbers"] == "7,11,22,29,38" def test_mm_ny_network_error(tmp_db): with patch("core.fetcher.requests.get", side_effect=req_lib.RequestException("refused")): result = fetch_megamillions_ny() assert result["status"] == "error" log = get_last_fetch_log("megamillions_ny") assert log["status"] == "error" # ── Mega Millions TX ────────────────────────────────────────────────────────── def test_mm_tx_inserts_records(tmp_db): with patch("core.fetcher.requests.get", return_value=_text_resp(MM_TX_CSV)): result = fetch_megamillions_tx() assert result["status"] == "success" assert result["added"] == 2 assert result["skipped"] == 0 def test_mm_tx_skips_duplicates(tmp_db): for _ in range(2): with patch("core.fetcher.requests.get", return_value=_text_resp(MM_TX_CSV)): result = fetch_megamillions_tx() assert result["added"] == 0 assert result["skipped"] == 2 def test_mm_tx_parses_date_and_numbers(tmp_db): with patch("core.fetcher.requests.get", return_value=_text_resp(MM_TX_CSV)): fetch_megamillions_tx() game = get_game_by_name("Mega Millions") draws = get_draws(game["id"], order="ASC") assert draws[0]["draw_date"] == "2024-03-07" assert draws[0]["numbers"] == "10,20,30,40,50" assert draws[0]["bonus"] == "12" assert draws[0]["multiplier"] == "3" def test_mm_tx_cross_source_dedup(tmp_db): """TX record on same date as an already-inserted NY record is skipped.""" with patch("core.fetcher.requests.get", side_effect=[_json_resp(MM_NY_RECORDS[:1]), _json_resp([])]): fetch_megamillions_ny() # inserts 2024-02-01 overlapping_csv = ( "Game Name,Month,Day,Year,Num1,Num2,Num3,Num4,Num5,Mega Ball,Megaplier\n" "Mega Millions,02,01,2024,07,11,22,29,38,4,2\n" ) with patch("core.fetcher.requests.get", return_value=_text_resp(overlapping_csv)): result = fetch_megamillions_tx() assert result["skipped"] == 1 assert result["added"] == 0 def test_mm_tx_network_error(tmp_db): with patch("core.fetcher.requests.get", side_effect=req_lib.RequestException("connect")): result = fetch_megamillions_tx() assert result["status"] == "error" assert result["added"] == 0 def test_mm_tx_no_header_row(tmp_db): """CSV without a header row still parses correctly.""" no_header = "Mega Millions,03,07,2024,10,20,30,40,50,12,3\n" with patch("core.fetcher.requests.get", return_value=_text_resp(no_header)): result = fetch_megamillions_tx() assert result["added"] == 1 # ── fetch_all ───────────────────────────────────────────────────────────────── def test_fetch_all_returns_three_sources(tmp_db): # Both NY APIs stop after 1 call (2 records < NY_API_LIMIT), so 3 calls total with patch("core.fetcher.requests.get", side_effect=[ _json_resp(PB_NY_RECORDS), _json_resp(MM_NY_RECORDS), _text_resp(MM_TX_CSV), ]): results = fetch_all() assert len(results) == 3 sources = {r["source"] for r in results} assert sources == {"powerball_ny", "megamillions_ny", "megamillions_tx"} def test_fetch_all_aggregated_counts(tmp_db): with patch("core.fetcher.requests.get", side_effect=[ _json_resp(PB_NY_RECORDS), _json_resp(MM_NY_RECORDS), _text_resp(MM_TX_CSV), ]): results = fetch_all() by_source = {r["source"]: r for r in results} assert by_source["powerball_ny"]["added"] == 2 assert by_source["megamillions_ny"]["added"] == 2 assert by_source["megamillions_tx"]["added"] == 2 def test_fetch_all_continues_after_one_error(tmp_db): """If one source errors, the remaining sources still complete.""" error_result = {"source": "powerball_ny", "added": 0, "skipped": 0, "status": "error", "message": "timeout"} ny_result = {"source": "megamillions_ny", "added": 2, "skipped": 0, "status": "success", "message": None} tx_result = {"source": "megamillions_tx", "added": 2, "skipped": 0, "status": "success", "message": None} with ( patch("core.fetcher.fetch_powerball_ny", return_value=error_result), patch("core.fetcher.fetch_megamillions_ny", return_value=ny_result), patch("core.fetcher.fetch_megamillions_tx", return_value=tx_result), ): results = fetch_all() assert len(results) == 3 by_source = {r["source"]: r for r in results} assert by_source["powerball_ny"]["status"] == "error" assert by_source["megamillions_ny"]["status"] == "success" assert by_source["megamillions_tx"]["status"] == "success" # ── Fetch log ───────────────────────────────────────────────────────────────── def test_fetch_log_written_on_success(tmp_db): with patch("core.fetcher.requests.get", side_effect=[_json_resp(PB_NY_RECORDS), _json_resp([])]): fetch_powerball_ny() log = get_last_fetch_log("powerball_ny") assert log is not None assert log["status"] == "success" assert log["added"] == 2 assert log["skipped"] == 0 def test_fetch_log_written_on_error(tmp_db): with patch("core.fetcher.requests.get", side_effect=req_lib.RequestException("fail")): fetch_powerball_ny() log = get_last_fetch_log("powerball_ny") assert log is not None assert log["status"] == "error"