Spaces:
Running on Zero
Running on Zero
Download tests/test_app.py from spacedout-bits/Oracle: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_app.py
- Command line
-
hf download hf://spaces/spacedout-bits/Oracle/tests/test_app.py
-
curl -L -o test_app.py https://huggingface.co/spaces/spacedout-bits/Oracle/resolve/main/tests/test_app.py
10.4 kB
| """End-to-end webhook tests with a stubbed Telegram transport. | |
| Environment is configured at import time, before the app's lifespan calls | |
| ``get_settings()`` (which is lru_cached, so the first call wins). | |
| """ | |
| import os | |
| import tempfile | |
| os.environ.update( | |
| { | |
| "DATA_DIR": tempfile.mkdtemp(prefix="finbot-test-"), | |
| "TELEGRAM_BOT_TOKEN": "", # keeps lifespan from calling Telegram | |
| "TELEGRAM_WEBHOOK_SECRET": "test-secret", | |
| "ALLOWED_TELEGRAM_USER_IDS": "42", | |
| "HF_TOKEN": "", # deterministic paths only; no network | |
| "HF_DATASET_REPO": "", | |
| "DEFAULT_CURRENCY": "INR", | |
| "TIMEZONE": "Asia/Kolkata", | |
| } | |
| ) | |
| import pytest # noqa: E402 | |
| from fastapi.testclient import TestClient # noqa: E402 | |
| from app import app # noqa: E402 | |
| SECRET = {"X-Telegram-Bot-Api-Secret-Token": "test-secret"} | |
| class FakeTelegram: | |
| """Records what would have been sent instead of calling the Bot API.""" | |
| def __init__(self): | |
| self.username = "FinBotTest" | |
| self.messages: list[tuple[int, str]] = [] | |
| self.documents: list[tuple[int, str, bytes]] = [] | |
| self.enabled = True | |
| async def send_message(self, chat_id, text, parse_mode="HTML"): | |
| self.messages.append((chat_id, text)) | |
| async def send_document(self, chat_id, filename, data, caption=""): | |
| self.documents.append((chat_id, filename, data)) | |
| async def send_typing(self, chat_id): | |
| pass | |
| async def download(self, file_id): | |
| return b"", "file.bin" | |
| async def close(self): | |
| pass | |
| def last(self) -> str: | |
| return self.messages[-1][1] if self.messages else "" | |
| def client(): | |
| with TestClient(app) as test_client: | |
| # Swap the Telegram transport on the live runtime so nothing hits the | |
| # network; background tasks hold this same Runtime instance. | |
| rt = app.state.rt | |
| fake = FakeTelegram() | |
| rt.telegram = fake | |
| test_client.fake = fake | |
| # Isolate each test: empty the ledger and any pending imports. | |
| store = rt.services.store | |
| for expense in store.list_expenses(42): | |
| store.delete_expense(42, expense.id) | |
| rt.services.pending.clear() | |
| yield test_client | |
| def send(client, text, user_id=42, update_id=None): | |
| import random | |
| payload = { | |
| "update_id": update_id if update_id is not None else random.randint(1, 10**9), | |
| "message": { | |
| "from": {"id": user_id}, | |
| "chat": {"id": user_id}, | |
| "text": text, | |
| }, | |
| } | |
| return client.post("/telegram/webhook", json=payload, headers=SECRET) | |
| class TestEndpoints: | |
| def test_health(self, client): | |
| response = client.get("/health") | |
| assert response.status_code == 200 | |
| assert response.json()["status"] == "ok" | |
| def test_health_leaks_no_ledger_detail(self, client): | |
| # The Space may be public; liveness must not publish personal metrics. | |
| body = client.get("/health").json() | |
| assert set(body) == {"status", "version"} | |
| def test_index_serves_the_gradio_ui(self, client): | |
| response = client.get("/") | |
| assert response.status_code == 200 | |
| assert "Oracle" in response.text | |
| def test_index_leaks_no_ledger_detail(self, client): | |
| send(client, "coffee 250") | |
| text = client.get("/").text.lower() | |
| for leak in ("mirroring", "not configured", "hf_dataset_repo", "telegram_bot_token"): | |
| assert leak not in text | |
| def test_public_page_shows_only_the_landing_copy(self, client): | |
| # No token: a visitor must see the explainer, never anyone's numbers. | |
| send(client, "coffee 250") | |
| text = client.get("/").text | |
| assert "sign-in link" in text | |
| assert "₹250.00" not in text | |
| def test_diagnostics_requires_the_secret(self, client): | |
| assert client.get("/admin/diagnostics").status_code == 403 | |
| assert client.get( | |
| "/admin/diagnostics", headers={"X-Admin-Secret": "wrong"} | |
| ).status_code == 403 | |
| def test_diagnostics_with_the_secret(self, client): | |
| response = client.get("/admin/diagnostics", headers={"X-Admin-Secret": "test-secret"}) | |
| assert response.status_code == 200 | |
| assert response.json()["ok"] is True | |
| assert "rows" in response.json() | |
| class TestWebhookSecurity: | |
| def test_rejects_a_missing_secret(self, client): | |
| response = client.post("/telegram/webhook", json={}) | |
| assert response.status_code == 403 | |
| def test_rejects_a_wrong_secret(self, client): | |
| response = client.post( | |
| "/telegram/webhook", | |
| json={}, | |
| headers={"X-Telegram-Bot-Api-Secret-Token": "wrong"}, | |
| ) | |
| assert response.status_code == 403 | |
| def test_rejects_malformed_json(self, client): | |
| response = client.post("/telegram/webhook", content=b"not json", headers=SECRET) | |
| assert response.status_code == 400 | |
| def test_unauthorised_user_is_refused_and_told_their_id(self, client): | |
| send(client, "coffee 250", user_id=9999) | |
| assert "9999" in client.fake.last | |
| assert "allowlist" in client.fake.last | |
| def test_unauthorised_user_writes_nothing(self, client): | |
| send(client, "coffee 250", user_id=9999) | |
| assert app.state.rt.services.store.count(9999) == 0 | |
| def test_duplicate_update_is_processed_once(self, client): | |
| send(client, "coffee 250", update_id=555) | |
| send(client, "coffee 250", update_id=555) | |
| assert app.state.rt.services.store.count(42) == 1 | |
| class TestLoggingFlow: | |
| def test_plain_text_is_logged(self, client): | |
| response = send(client, "coffee 250") | |
| assert response.status_code == 200 | |
| assert "Logged" in client.fake.last | |
| assert "₹250.00" in client.fake.last | |
| assert app.state.rt.services.store.count(42) == 1 | |
| def test_text_without_an_amount_becomes_conversation(self, client): | |
| # Previously replied "I couldn't find an amount in that", which made the | |
| # bot feel like a form. Plain text now reaches the coach instead. | |
| send(client, "hello there") | |
| assert "couldn't find an amount" not in client.fake.last | |
| assert client.fake.last # the coach said something | |
| assert app.state.rt.services.store.count(42) == 0 | |
| def test_spend_command(self, client): | |
| send(client, "/spend 450 swiggy dinner") | |
| assert "Logged" in client.fake.last | |
| assert app.state.rt.services.store.count(42) == 1 | |
| def test_spend_without_args_explains(self, client): | |
| send(client, "/spend") | |
| assert "/spend 250 coffee" in client.fake.last | |
| def test_undo_removes_the_last_entry(self, client): | |
| send(client, "coffee 250") | |
| send(client, "/undo") | |
| assert "Removed" in client.fake.last | |
| assert app.state.rt.services.store.count(42) == 0 | |
| def test_undo_with_nothing_to_undo(self, client): | |
| send(client, "/undo") | |
| assert "Nothing to undo" in client.fake.last | |
| class TestSummaries: | |
| def test_today(self, client): | |
| send(client, "coffee 250") | |
| send(client, "300 uber") | |
| send(client, "/today") | |
| assert "₹550.00" in client.fake.last | |
| def test_month(self, client): | |
| send(client, "1000 rent") | |
| send(client, "/month") | |
| assert "₹1,000.00" in client.fake.last | |
| def test_empty_period(self, client): | |
| send(client, "/today") | |
| assert "Nothing logged" in client.fake.last | |
| def test_multi_currency_is_reported_separately(self, client): | |
| send(client, "coffee 250") | |
| send(client, "€40 dinner") | |
| send(client, "/today") | |
| # Two currencies must never be summed into one figure. | |
| assert "Also:" in client.fake.last | |
| def test_list(self, client): | |
| send(client, "coffee 250") | |
| send(client, "/list") | |
| assert "coffee" in client.fake.last | |
| def test_report_accepts_a_period(self, client): | |
| send(client, "/report last_month") | |
| assert client.fake.last | |
| class TestBudgets: | |
| def test_set_and_view(self, client): | |
| send(client, "/budget food_dining 8000") | |
| assert "Budget set" in client.fake.last | |
| send(client, "/budgets") | |
| assert "Food & Dining" in client.fake.last | |
| def test_budget_tracks_spending(self, client): | |
| send(client, "/budget food_dining 1000") | |
| send(client, "500 lunch") | |
| send(client, "/budgets") | |
| assert "50%" in client.fake.last | |
| def test_unknown_category_is_rejected(self, client): | |
| send(client, "/budget nonsense 500") | |
| assert "don't recognise" in client.fake.last | |
| def test_budget_without_amount(self, client): | |
| send(client, "/budget groceries") | |
| assert "need an amount" in client.fake.last | |
| def test_categories_listing(self, client): | |
| send(client, "/categories") | |
| assert "food_dining" in client.fake.last | |
| class TestCoreCommands: | |
| def test_start(self, client): | |
| send(client, "/start") | |
| assert "Hello" in client.fake.last | |
| def test_help_lists_registered_features(self, client): | |
| send(client, "/help") | |
| text = client.fake.last | |
| assert "/spend" in text | |
| assert "/budget" in text | |
| assert "/advice" in text | |
| def test_unknown_command(self, client): | |
| send(client, "/nonsense") | |
| assert "don't know" in client.fake.last | |
| def test_status_warns_about_ephemeral_storage(self, client): | |
| send(client, "/status") | |
| # HF_DATASET_REPO is unset in this environment, so it must say so. | |
| assert "ephemeral" in client.fake.last.lower() | |
| def test_whoami(self, client): | |
| send(client, "/whoami") | |
| assert "42" in client.fake.last | |
| def test_advice_works_without_inference(self, client): | |
| send(client, "500 lunch") | |
| send(client, "/advice") | |
| assert client.fake.last # deterministic observations still fire | |
| def test_export_sends_a_csv(self, client): | |
| send(client, "coffee 250") | |
| send(client, "/export") | |
| assert len(client.fake.documents) == 1 | |
| _chat, filename, payload = client.fake.documents[0] | |
| assert filename.endswith(".csv") | |
| assert b"coffee" in payload | |
| def test_export_with_nothing_logged(self, client): | |
| send(client, "/export") | |
| assert "Nothing to export" in client.fake.last | |