Include Loyalty/Communication/Sports Center backends and registry updates alongside production nginx and compose wiring. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
5.4 KiB
Python
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()
|