from datetime import datetime, timedelta, timezone from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from quart import Quart from quart_jwt_extended import JWTManager, create_access_token from tortoise import Tortoise from blueprints import integrations from blueprints.users.models import User from blueprints.ynab import tools from blueprints.ynab.sync import tool_data_freshness async def test_stale_ynab_status_hides_provider_error(monkeypatch): monkeypatch.setattr(integrations, "ynab_configured", lambda: True) monkeypatch.setattr( integrations, "_current_state", AsyncMock( return_value=SimpleNamespace( last_synced_at=datetime.now(timezone.utc) - timedelta(hours=1), last_error="Authorization: secret-provider-key", ) ), ) result = await integrations._ynab_status() assert result["status"] == "stale" assert result["last_success_at"] assert "secret-provider-key" not in str(result) async def test_fresh_ynab_status_is_ready(monkeypatch): monkeypatch.setattr(integrations, "ynab_configured", lambda: True) monkeypatch.setattr( integrations, "_current_state", AsyncMock( return_value=SimpleNamespace( last_synced_at=datetime.now(timezone.utc), last_error=None ) ), ) assert (await integrations._ynab_status())["status"] == "ready" async def test_stale_tool_data_warns_model_and_carries_artifact_provenance(): @tools.ynab_tool_errors async def result(): tool_data_freshness.set( {"system": "ynab", "as_of": "2026-10-01T00:00:00+00:00", "stale": True} ) return "Total: $100", {"kind": "ynab.summary", "data": {"total": 100}} text, artifact = await result() assert "cached budget data is stale" in text assert "2026-10-01" in text assert artifact["source_ref"]["stale"] is True assert artifact["data"] == {"total": 100} async def test_tool_freshness_does_not_leak_from_previous_call(): tool_data_freshness.set({"system": "ynab", "as_of": "old", "stale": True}) @tools.ynab_tool_errors async def result(): return "Dates must be YYYY-MM-DD." assert await result() == "Dates must be YYYY-MM-DD." @pytest.fixture async def status_app(monkeypatch): await Tortoise.init( db_url="sqlite://:memory:", modules={"models": ["blueprints.users.models"]} ) await Tortoise.generate_schemas() user = await User.create(username="owner", email="owner@example.com") app = Quart(__name__) app.config["JWT_SECRET_KEY"] = "test-secret" JWTManager(app) app.register_blueprint(integrations.integrations_blueprint) monkeypatch.setattr( integrations, "integration_statuses", AsyncMock( return_value=[ { "name": "YNAB", "status": "stale", "message": "Cached data", "last_success_at": None, } ] ), ) async with app.app_context(): token = create_access_token(identity=str(user.id)) yield app, token await Tortoise.close_connections() async def test_status_requires_access_token(status_app): app, token = status_app assert (await app.test_client().get("/api/integrations/")).status_code == 401 response = await app.test_client().get( "/api/integrations/", headers={"Authorization": f"Bearer {token}"} ) assert response.status_code == 200 assert (await response.get_json())["integrations"][0]["status"] == "stale"