Independent FastAPI backend for the MaskanX agentic growth platform. Includes the agent runtime, MCP client integrations (Meta Ads, LinkedIn, HubSpot, Tavily, Exa, xAI, Citedy, image generation), PostgreSQL storage for chats and cron jobs, provider and secret management, and the CLI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
137 lines
3.7 KiB
Python
137 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from adclaw.app.routers import crm as crm_router
|
|
from adclaw.integrations import maskan_crm
|
|
from adclaw.integrations.maskan_crm import MaskanCRMClient
|
|
|
|
|
|
class _Response:
|
|
def __init__(self, payload: dict) -> None:
|
|
self.payload = payload
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_args):
|
|
return False
|
|
|
|
def read(self) -> bytes:
|
|
return json.dumps(self.payload).encode("utf-8")
|
|
|
|
|
|
def test_native_crm_client_normalizes_url_and_authenticates(monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_urlopen(request, timeout):
|
|
captured["url"] = request.full_url
|
|
captured["key"] = request.headers["X-integration-key"]
|
|
captured["timeout"] = timeout
|
|
return _Response(
|
|
{
|
|
"status": "ready",
|
|
"workspace": "maskan",
|
|
},
|
|
)
|
|
|
|
monkeypatch.setattr(maskan_crm, "urlopen", fake_urlopen)
|
|
client = MaskanCRMClient(
|
|
"http://crm.internal:8091",
|
|
"mcrm_test-key",
|
|
)
|
|
|
|
result = client.status()
|
|
|
|
assert result["status"] == "ready"
|
|
assert captured["url"] == (
|
|
"http://crm.internal:8091/api/v1/integrations/status"
|
|
)
|
|
assert captured["key"] == "mcrm_test-key"
|
|
assert captured["timeout"] == 15.0
|
|
|
|
|
|
def test_native_crm_lead_uses_idempotency_header(monkeypatch):
|
|
captured = {}
|
|
|
|
def fake_urlopen(request, timeout):
|
|
captured["idempotency_key"] = request.headers["Idempotency-key"]
|
|
captured["payload"] = json.loads(request.data.decode("utf-8"))
|
|
return _Response(
|
|
{
|
|
"contact_id": "contact-1",
|
|
"lead_id": "lead-1",
|
|
"created": True,
|
|
},
|
|
)
|
|
|
|
monkeypatch.setattr(maskan_crm, "urlopen", fake_urlopen)
|
|
client = MaskanCRMClient(
|
|
"https://crm.example.com/api/v1",
|
|
"mcrm_test-key",
|
|
)
|
|
|
|
result = client.create_lead(
|
|
{
|
|
"provider": "linkedin",
|
|
"external_id": "member-123",
|
|
"first_name": "Mahir",
|
|
},
|
|
idempotency_key="linkedin:member-123",
|
|
)
|
|
|
|
assert result["created"] is True
|
|
assert captured["idempotency_key"] == "linkedin:member-123"
|
|
assert captured["payload"]["provider"] == "linkedin"
|
|
|
|
|
|
def test_crm_config_preserves_a_saved_key_when_blank(monkeypatch):
|
|
existing = {
|
|
"MASKAN_CRM_API_URL": "http://old.example/api/v1",
|
|
"MASKAN_CRM_INTEGRATION_KEY": "mcrm_existing",
|
|
}
|
|
saved = {}
|
|
|
|
monkeypatch.setattr(crm_router, "load_envs", lambda: dict(existing))
|
|
monkeypatch.setattr(
|
|
crm_router,
|
|
"save_envs",
|
|
lambda values: saved.update(values),
|
|
)
|
|
monkeypatch.setattr(
|
|
crm_router,
|
|
"sync_maskan_crm_mcp_config",
|
|
lambda _values: None,
|
|
)
|
|
|
|
response = crm_router.update_crm_config(
|
|
crm_router.CRMConfigUpdate(
|
|
api_url="https://crm.example.com",
|
|
web_url="https://crm.example.com/app",
|
|
integration_key="",
|
|
),
|
|
)
|
|
|
|
assert response.key_configured is True
|
|
assert response.api_url == "https://crm.example.com/api/v1"
|
|
assert saved["MASKAN_CRM_INTEGRATION_KEY"] == "mcrm_existing"
|
|
|
|
|
|
def test_crm_config_never_returns_the_raw_key(monkeypatch):
|
|
monkeypatch.setattr(
|
|
crm_router,
|
|
"load_envs",
|
|
lambda: {
|
|
"MASKAN_CRM_API_URL": "https://crm.example.com/api/v1",
|
|
"MASKAN_CRM_INTEGRATION_KEY": "mcrm_secret",
|
|
},
|
|
)
|
|
|
|
response = crm_router.get_crm_config()
|
|
payload = response.model_dump()
|
|
|
|
assert payload["key_configured"] is True
|
|
assert "integration_key" not in payload
|
|
assert "mcrm_secret" not in str(payload)
|