mattrguerra/meridian-crm-mcpmainPrivate on GitHub, published here in full

test_party_writes.py

Language Python13.4 KB316 lines

tests/test_party_writes.py

  1. """Pins the payloads and replies of create_contact, update_contact,
  2. create_company and update_company, which share one internal implementation."""
  3. import asyncio
  4. import pytest
  5. from conftest import seq
  6. import server
  7. run = asyncio.run
  8. NO_DUP: dict = {}
  9. SAM = [{"ID": "7", "NAME": "Sam", "LAST_NAME": "Lee"}]
  10. CONTACT_FIELDS = {
  11. "fields": {
  12. "NAME": {"type": "string", "title": "First name"},
  13. "UF_CRM_MARKET": {
  14. "type": "enumeration",
  15. "title": "Market",
  16. "items": [{"ID": "44", "VALUE": "Government"}, {"ID": "45", "VALUE": "Retail"}],
  17. },
  18. }
  19. }
  20. COMPANY_FIELDS = {
  21. "fields": {
  22. "TITLE": {"type": "string", "title": "Company name"},
  23. "UF_CRM_TIER": {
  24. "type": "enumeration",
  25. "title": "Partner tier",
  26. "items": [{"ID": "61", "VALUE": "Palladium"}, {"ID": "62", "VALUE": "Iridium"}],
  27. },
  28. }
  29. }
  30. def fm(value):
  31. return [{"VALUE": value, "VALUE_TYPE": "WORK"}]
  32. # --------------------------------------------------------------------------- create_contact
  33. def test_create_contact_full_payload(fake_bitrix, logged):
  34. bx = fake_bitrix({
  35. "crm.duplicate.findbycomm": NO_DUP,
  36. "user.get": SAM,
  37. "crm.contact.add": 501,
  38. })
  39. out = run(server.create_contact(" Ana ", last_name="Reyes ", email=" ana@buyer.example ",
  40. phone="+1 555 010 2000", assignee="7"))
  41. assert out == "Created contact 501: Ana Reyes."
  42. assert bx.calls == [
  43. ("crm.duplicate.findbycomm", {"entity_type": "CONTACT", "type": "EMAIL", "values": ["ana@buyer.example"]}),
  44. ("crm.duplicate.findbycomm", {"entity_type": "CONTACT", "type": "PHONE", "values": ["+1 555 010 2000"]}),
  45. ("user.get", {"ID": "7"}),
  46. ("crm.contact.add", {"fields": {
  47. "NAME": "Ana",
  48. "LAST_NAME": "Reyes",
  49. "EMAIL": fm("ana@buyer.example"),
  50. "PHONE": fm("+1 555 010 2000"),
  51. "ASSIGNED_BY_ID": "7",
  52. }}),
  53. ]
  54. assert list(bx.calls[-1][1]["fields"]) == ["NAME", "LAST_NAME", "EMAIL", "PHONE", "ASSIGNED_BY_ID"]
  55. assert logged == [("create_contact", {"name": "Ana", "id": 501}, "created")]
  56. def test_create_contact_name_only(fake_bitrix, logged):
  57. bx = fake_bitrix({"crm.contact.add": {"id": 88}})
  58. out = run(server.create_contact("Ana"))
  59. assert out == "Created contact 88: Ana."
  60. assert bx.calls == [("crm.contact.add", {"fields": {"NAME": "Ana"}})]
  61. assert logged == [("create_contact", {"name": "Ana", "id": 88}, "created")]
  62. def test_create_contact_passes_through_unexpected_add_result(fake_bitrix, logged):
  63. fake_bitrix({"crm.contact.add": "90"})
  64. assert run(server.create_contact("Ana")) == "Created contact 90: Ana."
  65. def test_create_contact_returns_existing_duplicate(fake_bitrix, logged):
  66. bx = fake_bitrix({"crm.duplicate.findbycomm": seq(NO_DUP, {"CONTACT": [42, 43]})})
  67. out = run(server.create_contact("Ana", email="ana@buyer.example", phone="5550102000"))
  68. assert out == "Contact already exists (id 42, phone match); not creating a duplicate. Link it instead."
  69. assert bx.methods == ["crm.duplicate.findbycomm", "crm.duplicate.findbycomm"]
  70. assert logged == []
  71. def test_create_contact_dup_check_error_is_skipped(fake_bitrix, logged):
  72. bx = fake_bitrix({
  73. "crm.duplicate.findbycomm": server.BitrixError("ACCESS_DENIED: no"),
  74. "crm.contact.add": 3,
  75. })
  76. assert run(server.create_contact("Ana", email="ana@buyer.example")) == "Created contact 3: Ana."
  77. assert bx.methods == ["crm.duplicate.findbycomm", "crm.contact.add"]
  78. def test_create_contact_requires_name(fake_bitrix, logged):
  79. bx = fake_bitrix({})
  80. assert run(server.create_contact(" \x00 ")) == "Refused: a contact name is required."
  81. assert bx.calls == []
  82. def test_create_contact_unknown_assignee(fake_bitrix, logged):
  83. bx = fake_bitrix({"user.search": []})
  84. out = run(server.create_contact("Ana", assignee="Nobody"))
  85. assert out == "No Bitrix user found matching 'Nobody'."
  86. assert bx.calls == [("user.search", {"FILTER": {"FIND": "Nobody"}})]
  87. assert logged == []
  88. def test_create_contact_add_error(fake_bitrix, logged):
  89. fake_bitrix({"crm.contact.add": server.BitrixError("INVALID: bad")})
  90. assert run(server.create_contact("Ana")) == "Error creating contact: INVALID: bad"
  91. assert logged == [("create_contact", {"name": "Ana"}, "error: INVALID: bad")]
  92. # --------------------------------------------------------------------------- create_company
  93. def test_create_company_full_payload(fake_bitrix, logged):
  94. bx = fake_bitrix({
  95. "crm.duplicate.findbycomm": NO_DUP,
  96. "user.current": {"ID": "9", "NAME": "Me"},
  97. "crm.company.add": 700,
  98. })
  99. out = run(server.create_company(" Harbor Signs ", email="hello@harbor.example",
  100. phone="555-010-3000", assignee="me"))
  101. assert out == "Created company 700: Harbor Signs."
  102. assert bx.calls == [
  103. ("crm.duplicate.findbycomm", {"entity_type": "COMPANY", "type": "EMAIL", "values": ["hello@harbor.example"]}),
  104. ("crm.duplicate.findbycomm", {"entity_type": "COMPANY", "type": "PHONE", "values": ["555-010-3000"]}),
  105. ("user.current", None),
  106. ("crm.company.add", {"fields": {
  107. "TITLE": "Harbor Signs",
  108. "EMAIL": fm("hello@harbor.example"),
  109. "PHONE": fm("555-010-3000"),
  110. "ASSIGNED_BY_ID": "9",
  111. }}),
  112. ]
  113. assert list(bx.calls[-1][1]["fields"]) == ["TITLE", "EMAIL", "PHONE", "ASSIGNED_BY_ID"]
  114. assert logged == [("create_company", {"title": "Harbor Signs", "id": 700}, "created")]
  115. def test_create_company_returns_existing_duplicate(fake_bitrix, logged):
  116. fake_bitrix({"crm.duplicate.findbycomm": {"COMPANY": ["12"]}})
  117. out = run(server.create_company("Harbor Signs", email="hello@harbor.example"))
  118. assert out == "Company already exists (id 12, email match); not creating a duplicate. Link it instead."
  119. assert logged == []
  120. def test_create_company_requires_title(fake_bitrix, logged):
  121. bx = fake_bitrix({})
  122. assert run(server.create_company(" ")) == "Refused: a company name is required."
  123. assert bx.calls == []
  124. def test_create_company_numeric_assignee_is_taken_as_id(fake_bitrix, logged):
  125. # A numeric assignee is used as the id even when user.get finds nobody.
  126. bx = fake_bitrix({"user.get": [], "crm.company.add": 5})
  127. assert run(server.create_company("Harbor", assignee="31")) == "Created company 5: Harbor."
  128. assert bx.calls[-1] == ("crm.company.add", {"fields": {"TITLE": "Harbor", "ASSIGNED_BY_ID": "31"}})
  129. def test_create_company_add_error(fake_bitrix, logged):
  130. fake_bitrix({"crm.company.add": server.BitrixError("INVALID: bad")})
  131. assert run(server.create_company("Harbor")) == "Error creating company: INVALID: bad"
  132. assert logged == [("create_company", {"title": "Harbor"}, "error: INVALID: bad")]
  133. # --------------------------------------------------------------------------- update_contact
  134. def test_update_contact_full_payload(fake_bitrix, logged):
  135. bx = fake_bitrix({
  136. "user.get": SAM,
  137. "crm.item.fields": CONTACT_FIELDS,
  138. "crm.contact.update": True,
  139. })
  140. out = run(server.update_contact(" 12 ", name="Ana", last_name="Reyes", email="ana@buyer.example",
  141. phone="5550102000", assignee="7", fields={"Market": "Government"}))
  142. assert out == "Updated contact 12: NAME, LAST_NAME, EMAIL, PHONE, ASSIGNED_BY_ID, Market=Government."
  143. assert bx.calls == [
  144. ("user.get", {"ID": "7"}),
  145. ("crm.item.fields", {"entityTypeId": 3, "useOriginalUfNames": "Y"}),
  146. ("crm.contact.update", {"id": "12", "fields": {
  147. "NAME": "Ana",
  148. "LAST_NAME": "Reyes",
  149. "EMAIL": fm("ana@buyer.example"),
  150. "PHONE": fm("5550102000"),
  151. "ASSIGNED_BY_ID": "7",
  152. "UF_CRM_MARKET": "44",
  153. }}),
  154. ]
  155. assert list(bx.calls[-1][1]["fields"]) == [
  156. "NAME", "LAST_NAME", "EMAIL", "PHONE", "ASSIGNED_BY_ID", "UF_CRM_MARKET"]
  157. fields_logged = ["NAME", "LAST_NAME", "EMAIL", "PHONE", "ASSIGNED_BY_ID", "UF_CRM_MARKET"]
  158. assert logged == [("update_contact", {"record_id": "12", "fields": fields_logged}, "updated")]
  159. def test_update_contact_single_field(fake_bitrix, logged):
  160. bx = fake_bitrix({"crm.contact.update": True})
  161. assert run(server.update_contact("12", phone="5550102000")) == "Updated contact 12: PHONE."
  162. assert bx.calls == [("crm.contact.update", {"id": "12", "fields": {"PHONE": fm("5550102000")}})]
  163. def test_update_contact_rejects_standard_field_in_fields(fake_bitrix, logged):
  164. bx = fake_bitrix({"crm.item.fields": CONTACT_FIELDS})
  165. out = run(server.update_contact("12", fields={"First name": "Ana"}))
  166. assert out == ("Refused: NAME are standard fields; use the name/email/phone args "
  167. "instead of `fields`.")
  168. assert bx.methods == ["crm.item.fields"]
  169. def test_update_contact_unknown_enum_option(fake_bitrix, logged):
  170. fake_bitrix({"crm.item.fields": CONTACT_FIELDS})
  171. out = run(server.update_contact("12", fields={"Market": "Space"}))
  172. assert out == "'Space' is not an option of UF_CRM_MARKET. Options: Government | Retail"
  173. def test_update_contact_nothing_to_update(fake_bitrix, logged):
  174. bx = fake_bitrix({})
  175. out = run(server.update_contact("12", name=" "))
  176. assert out == "Nothing to update: pass a name/email/phone/assignee or fields."
  177. assert bx.calls == []
  178. def test_update_contact_invalid_id(fake_bitrix, logged):
  179. bx = fake_bitrix({})
  180. out = run(server.update_contact("12a", name="Ana"))
  181. assert out == "Invalid record_id '12a': a CRM record ID is a positive integer."
  182. assert bx.calls == []
  183. def test_update_contact_unknown_assignee(fake_bitrix, logged):
  184. bx = fake_bitrix({"user.search": []})
  185. out = run(server.update_contact("12", name="Ana", assignee="Nobody"))
  186. assert out == "No Bitrix user found matching 'Nobody'."
  187. assert bx.methods == ["user.search"]
  188. def test_update_contact_update_error(fake_bitrix, logged):
  189. fake_bitrix({"crm.contact.update": server.BitrixError("NOT_FOUND: gone")})
  190. out = run(server.update_contact("12", name="Ana"))
  191. assert out == "Error updating contact 12: NOT_FOUND: gone"
  192. assert logged == [("update_contact", {"record_id": "12", "fields": ["NAME"]}, "error: NOT_FOUND: gone")]
  193. # --------------------------------------------------------------------------- update_company
  194. def test_update_company_full_payload(fake_bitrix, logged):
  195. bx = fake_bitrix({
  196. "user.search": [{"ID": "7", "NAME": "Sam", "LAST_NAME": "Lee"}],
  197. "crm.item.fields": COMPANY_FIELDS,
  198. "crm.company.update": True,
  199. })
  200. out = run(server.update_company("40", title=" Harbor Signs ", email="hello@harbor.example",
  201. phone="555-010-3000", assignee="Sam", fields={"Partner tier": "Iridium"}))
  202. assert out == "Updated company 40: TITLE, EMAIL, PHONE, ASSIGNED_BY_ID, Partner tier=Iridium."
  203. assert bx.calls == [
  204. ("user.search", {"FILTER": {"FIND": "Sam"}}),
  205. ("crm.item.fields", {"entityTypeId": 4, "useOriginalUfNames": "Y"}),
  206. ("crm.company.update", {"id": "40", "fields": {
  207. "TITLE": "Harbor Signs",
  208. "EMAIL": fm("hello@harbor.example"),
  209. "PHONE": fm("555-010-3000"),
  210. "ASSIGNED_BY_ID": "7",
  211. "UF_CRM_TIER": "62",
  212. }}),
  213. ]
  214. fields_logged = ["TITLE", "EMAIL", "PHONE", "ASSIGNED_BY_ID", "UF_CRM_TIER"]
  215. assert logged == [("update_company", {"record_id": "40", "fields": fields_logged}, "updated")]
  216. def test_update_company_rejects_standard_field_in_fields(fake_bitrix, logged):
  217. fake_bitrix({"crm.item.fields": COMPANY_FIELDS})
  218. out = run(server.update_company("40", fields={"Company name": "X"}))
  219. assert out == ("Refused: TITLE are standard fields; use the title/email/phone args "
  220. "instead of `fields`.")
  221. def test_update_company_nothing_to_update(fake_bitrix, logged):
  222. out = run(server.update_company("40"))
  223. assert out == "Nothing to update: pass a title/email/phone/assignee or fields."
  224. def test_update_company_invalid_id(fake_bitrix, logged):
  225. bx = fake_bitrix({})
  226. out = run(server.update_company("-4", title="Harbor"))
  227. assert out == "Invalid record_id '-4': a CRM record ID is a positive integer."
  228. assert bx.calls == []
  229. def test_update_company_update_error(fake_bitrix, logged):
  230. fake_bitrix({"crm.company.update": server.BitrixError("NOT_FOUND: gone")})
  231. out = run(server.update_company("40", title="Harbor"))
  232. assert out == "Error updating company 40: NOT_FOUND: gone"
  233. assert logged == [("update_company", {"record_id": "40", "fields": ["TITLE"]}, "error: NOT_FOUND: gone")]
  234. # --------------------------------------------------------------------------- gates
  235. @pytest.mark.parametrize("call", [
  236. lambda: server.create_contact("Ana"),
  237. lambda: server.update_contact("12", name="Ana"),
  238. lambda: server.create_company("Harbor"),
  239. lambda: server.update_company("40", title="Harbor"),
  240. ])
  241. def test_party_writes_respect_both_switches(call, fake_bitrix, logged, monkeypatch):
  242. bx = fake_bitrix({})
  243. monkeypatch.setattr(server, "WRITES_ENABLED", False)
  244. assert run(call()) == "Writes are disabled on this server (BITRIX_WRITES=off). No change was made."
  245. monkeypatch.setattr(server, "WRITES_ENABLED", True)
  246. monkeypatch.setattr(server, "PARTY_WRITES_ENABLED", False)
  247. assert run(call()) == ("Contact/company writes are disabled (BITRIX_WRITE_PARTIES=off). "
  248. "No change was made.")
  249. assert bx.calls == []