Spaces:
Sleeping
Sleeping
File size: 7,412 Bytes
b81a86b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 | """
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"
)
|