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"
            )