TorbatYar/backend/services/communication/app/tests/test_architecture.py
Mortezakoohjani e41ecfad4c Sync platform docs, infra, and module services with Accounting integration.
Include Loyalty/Communication/Sports Center backends and registry updates alongside production nginx and compose wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-25 22:35:23 +03:30

158 lines
5.4 KiB
Python

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