import json from datetime import datetime, timedelta from uuid import uuid4 import pytest from sqlalchemy import create_engine from sqlalchemy.orm import Session from app.core.config import get_settings from app.core.database import Base from app.modules.feishu.app_tickets import FeishuAppTicketService from app.modules.feishu_users.models import FeishuUser from app.modules.observability.constants import ( ObservabilityKey, ObservabilityStatus, ) from app.modules.observability.service import ObservabilityService from app.modules.subscriptions.constants import ( PushDeliveryStatus, PushSubscriptionStatus, SubscriptionScheduleType, SubscriptionTargetType, ) from app.modules.subscriptions.models import PushDelivery, PushSubscription @pytest.fixture(autouse=True) def _reset_settings() -> None: get_settings.cache_clear() yield get_settings.cache_clear() def _add_active_subscription( db: Session, *, tenant_key: str, suffix: str, ) -> PushSubscription: owner = FeishuUser( code=f"FSU-{suffix}", tenant_key=tenant_key, open_id=f"open-{suffix}", ) db.add(owner) db.flush() subscription = PushSubscription( code=f"SUB-{suffix}", owner_id=owner.id, target_type=SubscriptionTargetType.USER, target_id=owner.open_id, prompt="发送个人提醒", schedule_type=SubscriptionScheduleType.DAILY, schedule_config={"hour": 9, "minute": 0}, timezone="Asia/Shanghai", next_run_at=datetime(2026, 7, 27, 1, 0), status=PushSubscriptionStatus.ACTIVE, consented_at=datetime(2026, 7, 26, 1, 0), ) db.add(subscription) db.commit() db.refresh(subscription) return subscription def _add_delivery( db: Session, subscription: PushSubscription, *, suffix: str, status_value: str, scheduled_for: datetime, ) -> None: future = datetime(2030, 1, 1, 0, 0) db.add( PushDelivery( code=f"DEL-{suffix}", subscription_id=subscription.id, scheduled_for=scheduled_for, idempotency_key=uuid4().hex + uuid4().hex, message_uuid=str(uuid4()), status=status_value, next_attempt_at=( future if status_value in { PushDeliveryStatus.PENDING, PushDeliveryStatus.RETRY, } else None ), locked_by=( "readiness-worker" if status_value == PushDeliveryStatus.PROCESSING else None ), locked_until=( future if status_value == PushDeliveryStatus.PROCESSING else None ), ) ) db.commit() def test_subscription_readiness_requires_basic_credentials( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("FEISHU_APP_TYPE", "self") monkeypatch.setenv("FEISHU_APP_ID", "") monkeypatch.setenv("FEISHU_APP_SECRET", "") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: _add_active_subscription(db, tenant_key="tenant-a", suffix="missing") result = ObservabilityService(db)._feishu_subscriptions_check() assert result[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert result["credentials_configured"] is False assert result["reasons"] == ["credentials_missing"] finally: engine.dispose() def test_self_app_readiness_rejects_multiple_active_tenants( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("FEISHU_APP_TYPE", "self") monkeypatch.setenv("FEISHU_APP_ID", "cli-self") monkeypatch.setenv("FEISHU_APP_SECRET", "secret") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: _add_active_subscription(db, tenant_key="tenant-a", suffix="self-a") single = ObservabilityService(db)._feishu_subscriptions_check() assert single[ObservabilityKey.STATUS] == ObservabilityStatus.OK assert single["active_tenant_count"] == 1 _add_active_subscription(db, tenant_key="tenant-b", suffix="self-b") multiple = ObservabilityService(db)._feishu_subscriptions_check() assert multiple[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert multiple["active_tenant_count"] == 2 assert multiple["reasons"] == ["self_app_multiple_tenants"] finally: engine.dispose() def test_store_app_readiness_accepts_persisted_ticket_without_exposing_it( monkeypatch: pytest.MonkeyPatch, ) -> None: ticket = "readiness-ticket-secret" monkeypatch.setenv("FEISHU_APP_TYPE", "store") monkeypatch.setenv("FEISHU_APP_ID", "cli-store") monkeypatch.setenv("FEISHU_APP_SECRET", "secret") monkeypatch.setenv("FEISHU_APP_TICKET", "") monkeypatch.setenv("FEISHU_DEFAULT_TENANT_KEY", "tenant-a") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: _add_active_subscription(db, tenant_key="tenant-a", suffix="store") missing = ObservabilityService(db)._feishu_subscriptions_check() assert missing[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert missing["reasons"] == ["app_ticket_missing"] FeishuAppTicketService(db).store_verified("cli-store", ticket) configured = ObservabilityService(db)._feishu_subscriptions_check() assert configured[ObservabilityKey.STATUS] == ObservabilityStatus.OK assert configured["ticket_configured"] is True assert configured["default_tenant_configured"] is True assert configured["reasons"] == [] assert ticket not in json.dumps(configured) finally: engine.dispose() def test_store_app_readiness_requires_default_tenant( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("FEISHU_APP_TYPE", "store") monkeypatch.setenv("FEISHU_APP_ID", "cli-store") monkeypatch.setenv("FEISHU_APP_SECRET", "secret") monkeypatch.setenv("FEISHU_APP_TICKET", "environment-ticket") monkeypatch.setenv("FEISHU_DEFAULT_TENANT_KEY", "") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: _add_active_subscription(db, tenant_key="tenant-a", suffix="default") result = ObservabilityService(db)._feishu_subscriptions_check() assert result[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert result["ticket_configured"] is True assert result["default_tenant_configured"] is False assert result["reasons"] == ["default_tenant_missing"] finally: engine.dispose() def test_readiness_checks_credentials_for_processable_deliveries_without_active_plan( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("FEISHU_APP_TYPE", "self") monkeypatch.setenv("FEISHU_APP_ID", "") monkeypatch.setenv("FEISHU_APP_SECRET", "") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: subscription = _add_active_subscription( db, tenant_key="tenant-a", suffix="durable", ) subscription.status = PushSubscriptionStatus.COMPLETED subscription.next_run_at = None db.commit() scheduled_for = datetime(2026, 7, 27, 1, 0) for index, status_value in enumerate( [ PushDeliveryStatus.PENDING, PushDeliveryStatus.RETRY, PushDeliveryStatus.PROCESSING, ] ): _add_delivery( db, subscription, suffix=f"durable-{index}", status_value=status_value, scheduled_for=scheduled_for + timedelta(minutes=index), ) result = ObservabilityService(db)._feishu_subscriptions_check() assert result[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert result["active"] == 0 assert result["processable_deliveries"] == 3 assert result["active_tenant_count"] == 1 assert result["reasons"] == ["credentials_missing"] finally: engine.dispose() def test_readiness_ignores_terminal_deliveries_without_active_plan() -> None: engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: subscription = _add_active_subscription( db, tenant_key="tenant-a", suffix="terminal", ) subscription.status = PushSubscriptionStatus.COMPLETED subscription.next_run_at = None db.commit() scheduled_for = datetime(2026, 7, 27, 1, 0) for index, status_value in enumerate( [ PushDeliveryStatus.SENT, PushDeliveryStatus.FAILED, PushDeliveryStatus.SKIPPED, ] ): _add_delivery( db, subscription, suffix=f"terminal-{index}", status_value=status_value, scheduled_for=scheduled_for + timedelta(minutes=index), ) result = ObservabilityService(db)._feishu_subscriptions_check() assert result == { ObservabilityKey.STATUS: ObservabilityStatus.SKIPPED, "active": 0, "processable_deliveries": 0, } finally: engine.dispose() def test_self_app_counts_tenants_from_processable_deliveries( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setenv("FEISHU_APP_TYPE", "self") monkeypatch.setenv("FEISHU_APP_ID", "cli-self") monkeypatch.setenv("FEISHU_APP_SECRET", "secret") engine = create_engine("sqlite://") Base.metadata.create_all(engine) try: with Session(engine) as db: scheduled_for = datetime(2026, 7, 27, 1, 0) for index, tenant_key in enumerate(["tenant-a", "tenant-b"]): subscription = _add_active_subscription( db, tenant_key=tenant_key, suffix=f"delivery-tenant-{index}", ) subscription.status = PushSubscriptionStatus.COMPLETED subscription.next_run_at = None db.commit() _add_delivery( db, subscription, suffix=f"delivery-tenant-{index}", status_value=PushDeliveryStatus.PENDING, scheduled_for=scheduled_for + timedelta(minutes=index), ) result = ObservabilityService(db)._feishu_subscriptions_check() assert result[ObservabilityKey.STATUS] == ObservabilityStatus.DEGRADED assert result["active"] == 0 assert result["processable_deliveries"] == 2 assert result["active_tenant_count"] == 2 assert result["reasons"] == ["self_app_multiple_tenants"] finally: engine.dispose()