Spaces:
Sleeping
Sleeping
| """ | |
| Tests for db/db_loader.py and DATA_BACKEND env-switch in server.py. | |
| These tests run fully offline — no Supabase/Postgres connection is made. | |
| Coverage: | |
| (a) Import: db.db_loader is importable without any env vars set. | |
| (b) No-env error: load_all() raises RuntimeError with a clear message | |
| when SUPABASE_DB_URL is absent. | |
| (c) CSV default: server._load_data_backend() returns CSV data when | |
| DATA_BACKEND is not set (or is 'csv'). | |
| (d) Backend dispatch: DATA_BACKEND=postgres routes to db_loader.load_all(). | |
| (e) Return-key contract: both backends return the same 6 top-level keys. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| # Ensure project root on path (same pattern as other tests) | |
| _HERE = os.path.dirname(os.path.abspath(__file__)) | |
| _ROOT = os.path.dirname(_HERE) | |
| if _ROOT not in sys.path: | |
| sys.path.insert(0, _ROOT) | |
| import pytest | |
| # ============================================================================= | |
| # (a) Import succeeds without any env vars | |
| # ============================================================================= | |
| class TestImport: | |
| def test_db_loader_importable(self): | |
| """db.db_loader must be importable regardless of env state.""" | |
| import db.db_loader as m # noqa: F401 | |
| assert hasattr(m, "load_all"), "load_all function must exist" | |
| def test_db_package_importable(self): | |
| """db/__init__.py must be importable.""" | |
| import db # noqa: F401 | |
| # ============================================================================= | |
| # (b) load_all() raises RuntimeError when SUPABASE_DB_URL is absent | |
| # ============================================================================= | |
| class TestNoEnvRaisesRuntimeError: | |
| def test_missing_env_raises(self, monkeypatch): | |
| """load_all() must raise RuntimeError with a descriptive message.""" | |
| # Guarantee the env var is absent for this test | |
| monkeypatch.delenv("SUPABASE_DB_URL", raising=False) | |
| from db.db_loader import load_all | |
| with pytest.raises(RuntimeError) as exc_info: | |
| load_all() | |
| msg = str(exc_info.value) | |
| assert "SUPABASE_DB_URL" in msg, ( | |
| "RuntimeError should name the missing env var so the operator knows what to set" | |
| ) | |
| def test_empty_string_env_raises(self, monkeypatch): | |
| """An empty SUPABASE_DB_URL must be treated the same as absent.""" | |
| monkeypatch.setenv("SUPABASE_DB_URL", "") | |
| from db.db_loader import load_all | |
| with pytest.raises(RuntimeError) as exc_info: | |
| load_all() | |
| assert "SUPABASE_DB_URL" in str(exc_info.value) | |
| # ============================================================================= | |
| # (c) CSV default path — server._load_data_backend() uses CSV when | |
| # DATA_BACKEND is unset or 'csv' | |
| # ============================================================================= | |
| EXPECTED_KEYS = {"kabupaten", "komoditas", "surplus", "deficit", "weather", "historical_prices"} | |
| class TestCsvDefaultPath: | |
| def test_unset_data_backend_uses_csv(self, monkeypatch): | |
| """Without DATA_BACKEND env, _load_data_backend() returns CSV data.""" | |
| monkeypatch.delenv("DATA_BACKEND", raising=False) | |
| # Re-import so the function picks up clean env | |
| import importlib | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| data = srv._load_data_backend() | |
| assert isinstance(data, dict) | |
| assert set(data.keys()) == EXPECTED_KEYS | |
| def test_explicit_csv_data_backend(self, monkeypatch): | |
| """DATA_BACKEND=csv must use CSV loader.""" | |
| monkeypatch.setenv("DATA_BACKEND", "csv") | |
| import importlib | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| data = srv._load_data_backend() | |
| assert isinstance(data, dict) | |
| assert set(data.keys()) == EXPECTED_KEYS | |
| def test_csv_data_has_content(self, monkeypatch): | |
| """CSV path must load at least one kabupaten and one komoditas.""" | |
| monkeypatch.delenv("DATA_BACKEND", raising=False) | |
| import importlib | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| data = srv._load_data_backend() | |
| assert len(data["kabupaten"]) > 0, "Expected kabupaten rows from CSV" | |
| assert len(data["komoditas"]) > 0, "Expected komoditas rows from CSV" | |
| assert len(data["surplus"]) > 0, "Expected surplus nodes from CSV" | |
| assert len(data["deficit"]) > 0, "Expected deficit nodes from CSV" | |
| # ============================================================================= | |
| # (d) Postgres backend routes to db_loader.load_all (verified via mock) | |
| # ============================================================================= | |
| class TestPostgresBackendDispatch: | |
| def test_postgres_backend_calls_db_loader(self, monkeypatch): | |
| """DATA_BACKEND=postgres must call db.db_loader.load_all, not CSV loader.""" | |
| monkeypatch.setenv("DATA_BACKEND", "postgres") | |
| monkeypatch.setenv("SUPABASE_DB_URL", "postgresql+psycopg2://fake:fake@localhost:5432/fake") | |
| call_log: list[str] = [] | |
| def fake_load_all(): | |
| call_log.append("db_loader.load_all called") | |
| # Return the same shape as the real loader so downstream code is happy | |
| from sample_data.loader import load_all_sample_data | |
| return load_all_sample_data() | |
| import importlib | |
| import db.db_loader | |
| monkeypatch.setattr(db.db_loader, "load_all", fake_load_all) | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| # Now call the dispatch function — it should route to our mock | |
| data = srv._load_data_backend() | |
| assert "db_loader.load_all called" in call_log, ( | |
| "DATA_BACKEND=postgres did not call db.db_loader.load_all" | |
| ) | |
| assert set(data.keys()) == EXPECTED_KEYS | |
| def test_postgres_backend_without_url_raises(self, monkeypatch): | |
| """DATA_BACKEND=postgres + missing SUPABASE_DB_URL => RuntimeError.""" | |
| monkeypatch.setenv("DATA_BACKEND", "postgres") | |
| monkeypatch.delenv("SUPABASE_DB_URL", raising=False) | |
| import importlib | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| with pytest.raises(RuntimeError) as exc_info: | |
| srv._load_data_backend() | |
| assert "SUPABASE_DB_URL" in str(exc_info.value) | |
| # ============================================================================= | |
| # (e) Return-key contract — both backends expose identical top-level keys | |
| # ============================================================================= | |
| class TestReturnKeyContract: | |
| def test_csv_keys_match_contract(self, monkeypatch): | |
| monkeypatch.delenv("DATA_BACKEND", raising=False) | |
| import importlib | |
| import whatsapp_bot.server as srv | |
| importlib.reload(srv) | |
| data = srv._load_data_backend() | |
| assert set(data.keys()) == EXPECTED_KEYS | |
| def test_db_loader_signature_documented(self): | |
| """db_loader.load_all docstring must reference all 6 keys.""" | |
| from db.db_loader import load_all | |
| doc = load_all.__doc__ or "" | |
| for key in EXPECTED_KEYS: | |
| assert key in doc, ( | |
| f"Key '{key}' missing from db_loader.load_all docstring — " | |
| "keep docs aligned with the return contract" | |
| ) | |