Files
upstream-frappe-crm/crm/domain_enrichment/tests/test_integration.py
upstream-mirror db69de1c11
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:36:01 +05:00

302 lines
11 KiB
Python

# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
"""IntegrationTestCase tests for the Frappe layer (real test DB).
Covers: seeder idempotency, the mapper write-policies against real CRM
Lead/Deal/Organization docs, the single Run writer, API permission/allow-list
enforcement + rate-limit, and the link-time Organization -> Lead/Deal copy.
All tests are network-free: wherever a crawl would happen the pipeline / fetch is
monkeypatched to return a canned EnrichmentResult, so nothing leaves the box.
"""
from __future__ import annotations
from unittest import mock
import frappe
from frappe.tests import IntegrationTestCase
from crm.domain_enrichment import config, install, tasks
from crm.domain_enrichment.config import get_config
from crm.domain_enrichment.cross_record import copy_enrichment_from_organization
from crm.domain_enrichment.mapper import apply_to_document
from crm.domain_enrichment.result import EnrichmentResult, Field, SocialProfile
from crm.domain_enrichment.tests import fixtures
def canned_result(website="https://acme.example") -> EnrichmentResult:
"""A fully-populated EnrichmentResult — no network, deterministic."""
r = EnrichmentResult(website=website)
r.company_name = Field("Acme Analytics Inc.", website, "JSON-LD")
r.description = Field("AI powered analytics SaaS for product teams.", website, "Meta Tag")
r.logo = Field("https://acme.example/favicon.ico", website, "Favicon")
r.industry = Field("SaaS", website, "Keyword Classifier")
r.industry_confidence = 0.8
r.social_profiles = {
"linkedin": SocialProfile("https://www.linkedin.com/company/acme", website),
"twitter": SocialProfile("https://twitter.com/acme", website),
}
return r
class SeederIdempotencyTest(IntegrationTestCase):
def tearDown(self):
frappe.db.rollback()
def test_seeder_is_a_no_op_on_second_run(self):
# The seeder runs on install/migrate, so records already exist. Running it
# again must not create duplicate Rule / Field Mapping rows.
install.seed_default_rules_and_mappings()
rules_before = frappe.db.count("CRM Enrichment Rule")
mappings_before = frappe.db.count("CRM Enrichment Field Mapping")
install.seed_default_rules_and_mappings()
self.assertEqual(frappe.db.count("CRM Enrichment Rule"), rules_before)
self.assertEqual(frappe.db.count("CRM Enrichment Field Mapping"), mappings_before)
self.assertGreater(rules_before, 0)
self.assertGreater(mappings_before, 0)
class MapperWritePolicyTest(IntegrationTestCase):
def setUp(self):
self.cfg = get_config()
self.result = canned_result()
def tearDown(self):
frappe.db.rollback()
def _new_org(self, **kwargs):
doc = frappe.new_doc("CRM Organization")
doc.organization_name = kwargs.pop("organization_name", "Mapper Test Org")
for k, v in kwargs.items():
doc.set(k, v)
return doc
def test_fill_if_empty_fills_only_empty_fields(self):
org = self._new_org(company_description="")
apply_to_document(org, self.result, self.cfg)
self.assertEqual(org.company_description, "AI powered analytics SaaS for product teams.")
def test_fill_if_empty_never_clobbers_user_data(self):
org = self._new_org(company_description="Hand-written by sales.")
apply_to_document(org, self.result, self.cfg)
self.assertEqual(org.company_description, "Hand-written by sales.")
def test_create_missing_link_creates_industry(self):
industry = "Quantum Widgets " + frappe.generate_hash(length=6)
self.assertFalse(frappe.db.exists("CRM Industry", industry))
result = canned_result()
result.industry = Field(industry, "https://x.example", "Keyword Classifier")
org = self._new_org(industry="")
apply_to_document(org, result, self.cfg)
self.assertTrue(frappe.db.exists("CRM Industry", industry))
self.assertEqual(org.industry, industry)
def test_create_missing_link_respects_user_create_permission(self):
# A user without CRM Industry create rights must not be able to auto-create a
# master from a scraped value: the master is not inserted and the field is
# skipped (no dangling link), rather than an ignore_permissions escalation.
industry = "Contraband Industry " + frappe.generate_hash(length=6)
result = canned_result()
result.industry = Field(industry, "https://x.example", "Keyword Classifier")
org = self._new_org(industry="")
user = _make_minimal_user()
self.addCleanup(frappe.set_user, "Administrator")
frappe.set_user(user)
apply_to_document(org, result, self.cfg)
self.assertFalse(frappe.db.exists("CRM Industry", industry))
self.assertFalse(org.industry)
class RunWriterTest(IntegrationTestCase):
def tearDown(self):
frappe.db.rollback()
def test_writes_exactly_one_run_with_summary(self):
org = frappe.new_doc("CRM Organization")
org.organization_name = "Run Writer Org " + frappe.generate_hash(length=4)
org.website = "https://acme.example"
org.insert()
before = frappe.db.count("CRM Enrichment Run", {"reference_name": org.name})
run_name = tasks.write_run(
"CRM Organization",
org.name,
"https://acme.example",
status="Completed",
result=canned_result(),
)
after = frappe.db.count("CRM Enrichment Run", {"reference_name": org.name})
self.assertEqual(after - before, 1)
run = frappe.get_doc("CRM Enrichment Run", run_name)
self.assertEqual(run.status, "Completed")
self.assertEqual(run.company_name, "Acme Analytics Inc.")
self.assertEqual(run.industry, "SaaS")
self.assertTrue(run.finished_on)
self.assertIn("Acme Analytics", run.raw_json)
class ApiPermissionTest(IntegrationTestCase):
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
def test_enrich_throws_on_disallowed_doctype(self):
from crm.domain_enrichment.api import enrich
with self.assertRaises(frappe.ValidationError):
enrich("ToDo", "anything")
def test_enrich_requires_write_permission(self):
from crm.domain_enrichment.api import enrich
org = frappe.new_doc("CRM Organization")
org.organization_name = "Perm Org " + frappe.generate_hash(length=4)
org.website = "https://acme.example"
org.insert()
# A user with no role on CRM Organization must be blocked by check_permission.
user = _make_minimal_user()
frappe.set_user(user)
with self.assertRaises(frappe.PermissionError):
enrich("CRM Organization", org.name)
def test_enrich_routes_are_rate_limited(self):
# enrich / retry each carry rate_limit(limit=ENRICH_RATE_LIMIT).
from crm.domain_enrichment import api
for fn in (api.enrich, api.retry):
self.assertTrue(hasattr(fn, "__wrapped__"), fn.__name__)
class CrossRecordCopyTest(IntegrationTestCase):
def setUp(self):
self.org = frappe.new_doc("CRM Organization")
self.org.organization_name = "Enriched Org " + frappe.generate_hash(length=4)
self.org.website = "https://acme.example"
self.org.company_description = "AI powered analytics SaaS."
self.org.linkedin = "https://www.linkedin.com/company/acme"
self.org.insert()
# Mark it enriched via a completed Run (the cleanest "is enriched" signal).
tasks.write_run(
"CRM Organization",
self.org.name,
self.org.website,
status="Completed",
result=canned_result(),
)
def tearDown(self):
frappe.db.rollback()
def test_copies_empty_fields_from_enriched_org(self):
deal = frappe.new_doc("CRM Deal")
deal.organization = self.org.name
filled = copy_enrichment_from_organization(deal)
self.assertTrue(filled)
self.assertEqual(deal.company_description, "AI powered analytics SaaS.")
self.assertEqual(deal.linkedin, "https://www.linkedin.com/company/acme")
def test_does_not_clobber_user_set_fields(self):
deal = frappe.new_doc("CRM Deal")
deal.organization = self.org.name
deal.company_description = "User wrote this."
copy_enrichment_from_organization(deal)
self.assertEqual(deal.company_description, "User wrote this.")
def test_non_enriched_org_is_a_noop(self):
plain = frappe.new_doc("CRM Organization")
plain.organization_name = "Plain Org " + frappe.generate_hash(length=4)
plain.website = "https://plain.example"
plain.insert()
deal = frappe.new_doc("CRM Deal")
deal.organization = plain.name
filled = copy_enrichment_from_organization(deal)
self.assertEqual(filled, [])
self.assertFalse(deal.company_description)
class AutoEnrichOnCreateTest(IntegrationTestCase):
"""The after_insert auto-enrich hook (Lead, Deal, Organization), gated by Settings."""
def _settings(self, **overrides):
s = {
"enabled": 1,
"auto_enrich": 1,
"request_timeout": 10,
"max_pages": 10,
}
s.update(overrides)
return frappe._dict(s)
def _doc(self, doctype, website="https://acme.example", name="X1"):
return frappe._dict(doctype=doctype, name=name, website=website)
def _run(self, doc, settings):
# auto_enrich_on_create now does a Settings-only gate (config.get_settings), and
# enqueue_enrichment reads its timeout the same way -- patch both to the fake
# Settings and assert on frappe.enqueue.
with (
mock.patch.object(config, "get_settings", return_value=settings),
mock.patch.object(tasks, "get_settings", return_value=settings),
mock.patch.object(tasks.frappe, "enqueue") as enq,
):
tasks.auto_enrich_on_create(doc)
return enq
def test_enqueues_for_new_lead(self):
enq = self._run(self._doc("CRM Lead", name="L1"), self._settings())
enq.assert_called_once()
kwargs = enq.call_args.kwargs
self.assertEqual(kwargs["reference_doctype"], "CRM Lead")
self.assertEqual(kwargs["job_id"], "domain-enrich-CRM Lead-L1")
def test_enqueues_for_new_deal(self):
enq = self._run(self._doc("CRM Deal", name="D1"), self._settings())
enq.assert_called_once()
kwargs = enq.call_args.kwargs
self.assertEqual(kwargs["reference_doctype"], "CRM Deal")
self.assertEqual(kwargs["job_id"], "domain-enrich-CRM Deal-D1")
self.assertTrue(kwargs["deduplicate"])
def test_enqueues_for_new_organization(self):
enq = self._run(self._doc("CRM Organization", name="O1"), self._settings())
enq.assert_called_once()
self.assertEqual(enq.call_args.kwargs["reference_doctype"], "CRM Organization")
def test_skips_when_auto_enrich_off(self):
enq = self._run(self._doc("CRM Deal"), self._settings(auto_enrich=0))
enq.assert_not_called()
def test_skips_when_feature_disabled(self):
enq = self._run(self._doc("CRM Deal"), self._settings(enabled=0))
enq.assert_not_called()
def test_skips_doctype_outside_the_allow_list(self):
enq = self._run(self._doc("CRM Task"), self._settings())
enq.assert_not_called()
def test_skips_without_website(self):
enq = self._run(self._doc("CRM Deal", website=""), self._settings())
enq.assert_not_called()
def _make_minimal_user():
"""Create (or reuse) a user with no CRM Organization role for permission tests."""
email = "de-noperm@example.com"
if not frappe.db.exists("User", email):
user = frappe.new_doc("User")
user.email = email
user.first_name = "No"
user.last_name = "Perm"
user.send_welcome_email = 0
user.insert(ignore_permissions=True)
return email