"""Architecture / boundary / documentation validation tests.""" from __future__ import annotations import ast from pathlib import Path from app.models.foundation import ( ContactSource, ManualContact, Message, MessageTemplate, OTPChallenge, ProviderConfig, QueueItem, SenderNumber, ) FORBIDDEN_IMPORT_PREFIXES = ( "backend.services.crm", "backend.services.accounting", "backend.services.loyalty", "backend.services.restaurant", "backend.services.marketplace", "backend.core_service", "app.services.crm", "app.models.crm", ) def test_all_models_have_tenant_id(): from app.core.database import Base import app.models # noqa: F401 skip = {"alembic_version"} for table in Base.metadata.tables.values(): if table.name in skip: continue assert "tenant_id" in table.columns, f"{table.name} missing tenant_id" def test_core_aggregates_independent(): for model in ( ProviderConfig, SenderNumber, MessageTemplate, ManualContact, ContactSource, Message, QueueItem, OTPChallenge, ): assert hasattr(model, "tenant_id") assert hasattr(model, "id") assert not model.__mapper__.relationships def test_permissions_defined(): from app.permissions.definitions import ALL_PERMISSIONS, PERMISSION_PREFIXES assert "communication.view" in ALL_PERMISSIONS assert "communication.providers.manage" in ALL_PERMISSIONS assert "communication.templates.approve" in ALL_PERMISSIONS assert "communication.messages.send" in ALL_PERMISSIONS assert "communication.otp.request" in ALL_PERMISSIONS assert "communication.queue.manage" in ALL_PERMISSIONS for prefix in PERMISSION_PREFIXES: assert any(p.startswith(prefix.rstrip(".")) or p.startswith(prefix) for p in ALL_PERMISSIONS) def test_events_defined(): from app.events.types import CommunicationEventType assert CommunicationEventType.MESSAGE_SENT.value == "communication.message.sent" assert CommunicationEventType.MESSAGE_DELIVERED.value == "communication.message.delivered" assert CommunicationEventType.PROVIDER_FAILOVER.value == "communication.provider.failover" assert CommunicationEventType.OTP_VERIFIED.value == "communication.otp.verified" assert CommunicationEventType.QUEUE_DEAD_LETTER.value == "communication.queue.dead_letter" def test_sports_contact_source_type(): from app.models.types import ContactSourceType assert ContactSourceType.SPORTS.value == "sports" def test_permission_enforcement_module_exists(): from app.api.permissions import require_permissions, user_has_permission assert callable(require_permissions) assert callable(user_has_permission) def test_provider_framework_registered(): from app.providers import list_registered_provider_kinds, supported_channels kinds = list_registered_provider_kinds() assert "mock" in kinds assert "payamak" in kinds channels = {c["channel"]: c for c in supported_channels()} assert channels["sms"]["status"] == "active" assert "email" in channels assert "whatsapp" in channels assert "telegram" in channels assert "rubika" in channels def test_no_forbidden_business_module_imports(): root = Path(__file__).resolve().parents[1] violations: list[str] = [] for path in root.rglob("*.py"): if "tests" in path.parts: continue tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: for forbidden in FORBIDDEN_IMPORT_PREFIXES: if alias.name.startswith(forbidden) or forbidden in alias.name: violations.append(f"{path}: import {alias.name}") elif isinstance(node, ast.ImportFrom) and node.module: for forbidden in FORBIDDEN_IMPORT_PREFIXES: if node.module.startswith(forbidden) or forbidden in node.module: violations.append(f"{path}: from {node.module}") assert violations == [] def test_folder_structure(): root = Path(__file__).resolve().parents[2] required = [ "app/models", "app/repositories", "app/services", "app/validators", "app/schemas", "app/events", "app/permissions", "app/providers", "app/api/v1", "app/tests", "alembic/versions", "README.md", ] for rel in required: assert (root / rel).exists(), f"missing {rel}" def test_phase_documentation_exists(): docs = Path(__file__).resolve().parents[4] / "docs" # repo root = parents[4] from app/tests -> communication -> services -> backend -> root # Path: .../communication/app/tests -> parents[0]=tests, [1]=app, [2]=communication, [3]=services, [4]=backend, [5]=root docs = Path(__file__).resolve().parents[5] / "docs" assert (docs / "communication-phase-8.md").exists() progress = (docs / "progress.md").read_text(encoding="utf-8") assert "Phase 8" in progress registry = (docs / "module-registry.md").read_text(encoding="utf-8") assert "communication" in registry.lower()