"""Architecture tests — CRM module boundary enforcement.""" from __future__ import annotations import ast from pathlib import Path from app.models.foundation import ( Contact, Lead, Opportunity, Organization, Pipeline, PipelineStage, Quote, SalesActivity, ) FORBIDDEN_IMPORT_PREFIXES = ( "app.services.accounting", "app.models.accounting", "backend.services.accounting", "backend.services.automation", "backend.services.notification", "backend.services.file_storage", "backend.services.identity_access", "backend.core_service", ) 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_foundation_aggregates_are_independent(): aggregates = { Lead, Contact, Organization, Opportunity, Pipeline, PipelineStage, SalesActivity, Quote, } assert len(aggregates) == 8 for model in aggregates: assert hasattr(model, "tenant_id") assert hasattr(model, "id") # No SQLAlchemy relationship graphs coupling aggregates assert not model.__mapper__.relationships def test_permissions_defined(): from app.permissions.definitions import ALL_PERMISSIONS, PERMISSION_PREFIXES assert "crm.view" in ALL_PERMISSIONS assert "crm.leads.create" in ALL_PERMISSIONS assert "crm.contacts.view" in ALL_PERMISSIONS assert "crm.organizations.manage" in ALL_PERMISSIONS assert "crm.opportunities.win" in ALL_PERMISSIONS assert "crm.pipelines.manage" in ALL_PERMISSIONS assert "crm.playbooks.assign" in ALL_PERMISSIONS assert "crm.forecasts.view" in ALL_PERMISSIONS assert "crm.goals.create" in ALL_PERMISSIONS assert "crm.targets.manage" in ALL_PERMISSIONS assert "crm.tasks.complete" in ALL_PERMISSIONS assert "crm.meetings.create" in ALL_PERMISSIONS assert "crm.calls.manage" in ALL_PERMISSIONS assert "crm.timeline.view" in ALL_PERMISSIONS assert "crm.comments.create" in ALL_PERMISSIONS assert "crm.mentions.view" in ALL_PERMISSIONS assert "crm.team.manage" in ALL_PERMISSIONS assert "crm.activities.complete" in ALL_PERMISSIONS assert "crm.quotes.create" in ALL_PERMISSIONS assert "crm.notes.create" in ALL_PERMISSIONS assert "crm.attachments.create" in ALL_PERMISSIONS assert "crm.customfields.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 CrmEventType assert CrmEventType.LEAD_CREATED.value == "crm.lead.created" assert CrmEventType.LEAD_UPDATED.value == "crm.lead.updated" assert CrmEventType.CONTACT_CREATED.value == "crm.contact.created" assert CrmEventType.ORGANIZATION_CREATED.value == "crm.organization.created" assert CrmEventType.OPPORTUNITY_CREATED.value == "crm.opportunity.created" assert CrmEventType.OPPORTUNITY_UPDATED.value == "crm.opportunity.updated" assert CrmEventType.OPPORTUNITY_WON.value == "crm.opportunity.won" assert CrmEventType.OPPORTUNITY_LOST.value == "crm.opportunity.lost" assert CrmEventType.PIPELINE_CHANGED.value == "crm.pipeline.changed" assert CrmEventType.STAGE_CHANGED.value == "crm.stage.changed" assert CrmEventType.PLAYBOOK_ASSIGNED.value == "crm.playbook.assigned" assert CrmEventType.PLAYBOOK_COMPLETED.value == "crm.playbook.completed" assert CrmEventType.FORECAST_UPDATED.value == "crm.forecast.updated" assert CrmEventType.ACTIVITY_COMPLETED.value == "crm.activity.completed" assert CrmEventType.TASK_COMPLETED.value == "crm.task.completed" assert CrmEventType.MEETING_FINISHED.value == "crm.meeting.finished" assert CrmEventType.CALL_LOGGED.value == "crm.call.logged" assert CrmEventType.TIMELINE_UPDATED.value == "crm.timeline.updated" assert CrmEventType.COMMENT_CREATED.value == "crm.comment.created" assert CrmEventType.MENTION_CREATED.value == "crm.mention.created" assert CrmEventType.QUOTE_CREATED.value == "crm.quote.created" assert CrmEventType.NOTE_CREATED.value == "crm.note.created" assert CrmEventType.ATTACHMENT_ADDED.value == "crm.attachment.added" def test_platform_provider_contracts_exist(): from app.providers import ( AIProvider, AnalyticsProvider, AutomationProvider, CommunicationProvider, Customer360Provider, FileStorageProvider, HelpdeskProvider, NotificationProvider, ProductServiceProvider, ) assert AutomationProvider is not None assert Customer360Provider is not None assert NotificationProvider is not None assert AnalyticsProvider is not None assert AIProvider is not None assert CommunicationProvider is not None assert HelpdeskProvider is not None assert FileStorageProvider is not None assert ProductServiceProvider is not None def test_no_forbidden_service_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_api_ownership_is_crm_only(): from app.api.v1 import api_router prefixes = {getattr(r, "path", "") for r in api_router.routes} joined = " ".join(sorted(prefixes)) assert "/leads" in joined or any("/leads" in p for p in prefixes) # Forbid platform-owned path segments (word boundaries), not substrings # (e.g. "ai" must not match "/emails"). import re forbidden = ( "automation", "customer360", "notification", "helpdesk", "analytics", "ai", ) for name in forbidden: assert not re.search(rf"(^|/){re.escape(name)}(/|$|\s)", joined), name 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}"