From b76159e3db7421b0eea4b33498370faa4adaef36 Mon Sep 17 00:00:00 2001 From: Lee Chapman Date: Sun, 29 Mar 2026 20:58:38 -0700 Subject: [PATCH] feat(api): warm war status and statistics when DB is empty DataCollector already runs collect_all_data in a background thread on startup; these GET handlers now fetch from the live scraper and persist when no row exists yet, so the first request can succeed before the initial collection finishes. Tests mock the scraper for 404 paths and cover live-fetch success. Made-with: Cursor --- src/app.py | 17 ++++++++++++---- tests/test_app.py | 40 +++++++++++++++++++++++++++++++++---- tests/test_coverage_gaps.py | 6 ++++-- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/app.py b/src/app.py index b27982a..9318445 100644 --- a/src/app.py +++ b/src/app.py @@ -60,11 +60,16 @@ async def lifespan(app: FastAPI): @app.get("/api/war/status", tags=["War"]) async def get_war_status(): - """Get current war status""" + """Get current war status. If DB is empty, fetches live and saves so Data Console/MCP get data on first request.""" data = db.get_latest_war_status() if data: return data - raise HTTPException(status_code=404, detail="No war status data available") + # No data yet (e.g. before first collector run): try live fetch so UI/MCP get something + data = scraper.get_war_status() + if data: + db.save_war_status(data) + return data + raise HTTPException(status_code=404, detail="No war status data available (upstream API may be unreachable)") @app.post("/api/war/status/refresh", tags=["War"]) @@ -316,11 +321,15 @@ async def get_planet_history(planet_index: int, limit: int = Query(10, ge=1, le= @app.get("/api/statistics", tags=["Statistics"]) async def get_statistics(): - """Get latest global statistics""" + """Get latest global statistics. If DB is empty, fetches live and saves so Data Console/MCP get data.""" data = db.get_latest_statistics() if data: return data - raise HTTPException(status_code=404, detail="No statistics available") + data = scraper.get_statistics() + if data: + db.save_statistics(data) + return data + raise HTTPException(status_code=404, detail="No statistics available (upstream API may be unreachable)") @app.get("/api/statistics/history", tags=["Statistics"]) diff --git a/tests/test_app.py b/tests/test_app.py index 2b20914..aafc5a6 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -50,14 +50,30 @@ def test_get_war_status_success(self, mock_get, client): data = response.json() assert data["war_id"] == 1 + @patch("src.app.scraper.get_war_status") @patch("src.app.db.get_latest_war_status") - def test_get_war_status_not_found(self, mock_get, client): - """Test getting war status when none exists""" + def test_get_war_status_not_found(self, mock_get, mock_scraper, client): + """Test getting war status when DB empty and live fetch fails""" mock_get.return_value = None + mock_scraper.return_value = None response = client.get("/api/war/status") assert response.status_code == 404 + @patch("src.app.scraper.get_war_status") + @patch("src.app.db.save_war_status") + @patch("src.app.db.get_latest_war_status") + def test_get_war_status_live_when_db_empty(self, mock_get, mock_save, mock_scraper, client): + """When DB has no row yet, GET uses live fetch and persists""" + mock_get.return_value = None + mock_scraper.return_value = {"war_id": 42, "status": "active"} + mock_save.return_value = True + + response = client.get("/api/war/status") + assert response.status_code == 200 + assert response.json()["war_id"] == 42 + mock_save.assert_called_once() + @patch("src.app.scraper.get_war_status") @patch("src.app.db.save_war_status") def test_refresh_war_status_success(self, mock_save, mock_scraper, client): @@ -173,14 +189,30 @@ def test_get_statistics_success(self, mock_get, client): response = client.get("/api/statistics") assert response.status_code == 200 + @patch("src.app.scraper.get_statistics") @patch("src.app.db.get_latest_statistics") - def test_get_statistics_not_found(self, mock_get, client): - """Test getting statistics when none exists""" + def test_get_statistics_not_found(self, mock_get, mock_scraper, client): + """Test getting statistics when DB empty and live fetch fails""" mock_get.return_value = None + mock_scraper.return_value = None response = client.get("/api/statistics") assert response.status_code == 404 + @patch("src.app.scraper.get_statistics") + @patch("src.app.db.save_statistics") + @patch("src.app.db.get_latest_statistics") + def test_get_statistics_live_when_db_empty(self, mock_get, mock_save, mock_scraper, client): + """When DB has no row yet, GET uses live fetch and persists""" + mock_get.return_value = None + mock_scraper.return_value = {"total_players": 999} + mock_save.return_value = True + + response = client.get("/api/statistics") + assert response.status_code == 200 + assert response.json()["total_players"] == 999 + mock_save.assert_called_once() + @patch("src.app.db.get_latest_statistics") def test_get_statistics_history(self, mock_get, client): """Test getting statistics history""" diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 4d6a71d..7e21301 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -187,10 +187,12 @@ def test_refresh_statistics_none(self, mock_save, mock_scraper, client): class TestDatabaseCoveragePaths: """Test database error and edge case paths""" + @patch("src.app.scraper.get_statistics") @patch("src.app.db.get_latest_statistics") - def test_statistics_not_found(self, mock_get, client): - """Test getting statistics when not found""" + def test_statistics_not_found(self, mock_get, mock_scraper, client): + """Test getting statistics when DB empty and live fetch fails""" mock_get.return_value = None + mock_scraper.return_value = None response = client.get("/api/statistics") assert response.status_code == 404